github.com/snyk/vervet/v5@v5.11.1-0.20240202085829-ad4dd7fb6101/config/api.go (about)

     1  package config
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/bmatcuk/doublestar/v4"
     7  )
     8  
     9  // APIs defines a named map of API instances.
    10  type APIs map[string]*API
    11  
    12  // An API defines how and where to build versioned OpenAPI documents from a
    13  // source collection of individual resource specifications and additional
    14  // overlay content to merge.
    15  type API struct {
    16  	Name      string         `json:"-"`
    17  	Resources []*ResourceSet `json:"resources"`
    18  	Overlays  []*Overlay     `json:"overlays"`
    19  	Output    *Output        `json:"output"`
    20  }
    21  
    22  // A ResourceSet defines a set of versioned resources that adhere to the same
    23  // standards.
    24  //
    25  // Versioned resources are expressed as individual OpenAPI documents in a
    26  // directory structure:
    27  //
    28  // +-resource
    29  //
    30  //	|
    31  //	+-2021-08-01
    32  //	| |
    33  //	| +-spec.yaml
    34  //	| +-<implementation code, etc. can go here>
    35  //	|
    36  //	+-2021-08-15
    37  //	| |
    38  //	| +-spec.yaml
    39  //	| +-<implementation code, etc. can go here>
    40  //	...
    41  //
    42  // Each YYYY-mm-dd directory under a resource is a version.  The spec.yaml
    43  // in each version is a complete OpenAPI document describing the resource
    44  // at that version.
    45  type ResourceSet struct {
    46  	Description     string             `json:"description"`
    47  	Linter          string             `json:"linter"`
    48  	LinterOverrides map[string]Linters `json:"linter-overrides"`
    49  	Path            string             `json:"path"`
    50  	Excludes        []string           `json:"excludes"`
    51  }
    52  
    53  func (r *ResourceSet) validate() error {
    54  	for _, exclude := range r.Excludes {
    55  		if !doublestar.ValidatePattern(exclude) {
    56  			return fmt.Errorf("invalid exclude pattern %q", exclude)
    57  		}
    58  	}
    59  	return nil
    60  }
    61  
    62  // An Overlay defines additional OpenAPI documents to merge into the aggregate
    63  // OpenAPI spec when compiling an API. These might include special endpoints
    64  // that should be included in the aggregate API but are not versioned, or
    65  // top-level descriptions of the API itself.
    66  type Overlay struct {
    67  	Include string `json:"include"`
    68  	Inline  string `json:"inline"`
    69  }
    70  
    71  // Output defines where the aggregate versioned OpenAPI specs should be created
    72  // during compilation.
    73  type Output struct {
    74  	Path   string   `json:"path,omitempty"`
    75  	Paths  []string `json:"paths,omitempty"`
    76  	Linter string   `json:"linter"`
    77  }
    78  
    79  // EffectivePaths returns a slice of effective configured output paths, whether
    80  // a single or multiple output paths have been configured.
    81  func (o *Output) ResolvePaths() []string {
    82  	if o.Path != "" {
    83  		return []string{o.Path}
    84  	}
    85  	return o.Paths
    86  }
    87  
    88  func (a APIs) init(p *Project) error {
    89  	if len(a) == 0 {
    90  		return fmt.Errorf("no apis defined")
    91  	}
    92  	// Referenced linters and generators all exist
    93  	for name, api := range a {
    94  		api.Name = name
    95  		if len(api.Resources) == 0 {
    96  			return fmt.Errorf("no resources defined (apis.%s.resources)", api.Name)
    97  		}
    98  		for rcIndex, resource := range api.Resources {
    99  			if resource.Linter != "" {
   100  				if _, ok := p.Linters[resource.Linter]; !ok {
   101  					return fmt.Errorf("linter %q not found (apis.%s.resources[%d].linter)",
   102  						resource.Linter, api.Name, rcIndex)
   103  				}
   104  			}
   105  			if err := resource.validate(); err != nil {
   106  				return fmt.Errorf("%w (apis.%s.resources[%d])", err, api.Name, rcIndex)
   107  			}
   108  			for rcName, versionMap := range resource.LinterOverrides {
   109  				for version, linter := range versionMap {
   110  					err := linter.validate()
   111  					if err != nil {
   112  						return fmt.Errorf("%w (apis.%s.resources[%d].linter-overrides.%s.%s)",
   113  							err, api.Name, rcIndex, rcName, version)
   114  					}
   115  					if linter.OpticCI != nil {
   116  						return fmt.Errorf("optic linter does not support overrides (apis.%s.resources[%d].linter-overrides.%s.%s)",
   117  							api.Name, rcIndex, rcName, version)
   118  					}
   119  				}
   120  			}
   121  		}
   122  		if api.Output != nil {
   123  			if len(api.Output.Paths) > 0 && api.Output.Path != "" {
   124  				return fmt.Errorf("output should specify one of 'path' or 'paths', not both (apis.%s.output)",
   125  					api.Name)
   126  			}
   127  			if api.Output.Linter != "" {
   128  				if linter, ok := p.Linters[api.Output.Linter]; !ok {
   129  					return fmt.Errorf("linter %q not found (apis.%s.output.linter)",
   130  						api.Output.Linter, api.Name)
   131  				} else if linter.OpticCI != nil {
   132  					return fmt.Errorf("optic linter does not yet support compiled specs (apis.%s.output.linter)",
   133  						api.Name)
   134  				}
   135  			}
   136  		}
   137  	}
   138  	return nil
   139  }