github.com/snyk/vervet/v6@v6.2.4/merge.go (about)

     1  package vervet
     2  
     3  import (
     4  	"errors"
     5  	"sort"
     6  
     7  	"github.com/getkin/kin-openapi/openapi3"
     8  )
     9  
    10  // Merge adds the paths and components from a source OpenAPI document root,
    11  // to a destination document root.
    12  //
    13  // TODO: This is a naive implementation that should be improved to detect and
    14  // resolve conflicts better. For example, distinct resources might have
    15  // localized references with the same URIs but different content.
    16  // Content-addressible resource versions may further facilitate governance;
    17  // this also would facilitate detecting and relocating such conflicts.
    18  //
    19  // TODO(next-release):
    20  //   - This function is suitable for overlay merging scenarios only.
    21  //   - Component merging should be removed. Use Collator for safe component
    22  //     merging.
    23  func Merge(dst, src *openapi3.T, replace bool) error {
    24  	err := mergeComponents(dst, src, replace)
    25  	if err != nil {
    26  		return err
    27  	}
    28  	mergeExtensions(dst, src, replace)
    29  	mergeInfo(dst, src, replace)
    30  	mergeOpenAPIVersion(dst, src, replace)
    31  	mergePaths(dst, src, replace)
    32  	mergeSecurityRequirements(dst, src, replace)
    33  	mergeServers(dst, src, replace)
    34  	mergeTags(dst, src, replace)
    35  
    36  	return nil
    37  }
    38  
    39  func mergeOpenAPIVersion(dst, src *openapi3.T, replace bool) {
    40  	if dst.OpenAPI == "" || (src.OpenAPI != "" && replace) {
    41  		dst.OpenAPI = src.OpenAPI
    42  	}
    43  }
    44  
    45  func mergeTags(dst, src *openapi3.T, replace bool) {
    46  	m := map[string]*openapi3.Tag{}
    47  	for _, t := range dst.Tags {
    48  		m[t.Name] = t
    49  	}
    50  	for _, t := range src.Tags {
    51  		if _, ok := m[t.Name]; !ok || replace {
    52  			m[t.Name] = t
    53  		}
    54  	}
    55  	dst.Tags = openapi3.Tags{}
    56  	tagNames := []string{}
    57  	for tagName := range m {
    58  		tagNames = append(tagNames, tagName)
    59  	}
    60  	sort.Strings(tagNames)
    61  	for _, tagName := range tagNames {
    62  		dst.Tags = append(dst.Tags, m[tagName])
    63  	}
    64  }
    65  
    66  func initDestinationComponents(dst, src *openapi3.T) {
    67  	if src.Components.Schemas != nil && dst.Components.Schemas == nil {
    68  		dst.Components.Schemas = make(map[string]*openapi3.SchemaRef)
    69  	}
    70  	if src.Components.Parameters != nil && dst.Components.Parameters == nil {
    71  		dst.Components.Parameters = make(map[string]*openapi3.ParameterRef)
    72  	}
    73  	if src.Components.Headers != nil && dst.Components.Headers == nil {
    74  		dst.Components.Headers = make(map[string]*openapi3.HeaderRef)
    75  	}
    76  	if src.Components.RequestBodies != nil && dst.Components.RequestBodies == nil {
    77  		dst.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef)
    78  	}
    79  	if src.Components.Responses != nil && dst.Components.Responses == nil {
    80  		dst.Components.Responses = make(map[string]*openapi3.ResponseRef)
    81  	}
    82  	if src.Components.SecuritySchemes != nil && dst.Components.SecuritySchemes == nil {
    83  		dst.Components.SecuritySchemes = make(map[string]*openapi3.SecuritySchemeRef)
    84  	}
    85  	if src.Components.Examples != nil && dst.Components.Examples == nil {
    86  		dst.Components.Examples = make(map[string]*openapi3.ExampleRef)
    87  	}
    88  	if src.Components.Links != nil && dst.Components.Links == nil {
    89  		dst.Components.Links = make(map[string]*openapi3.LinkRef)
    90  	}
    91  	if src.Components.Callbacks != nil && dst.Components.Callbacks == nil {
    92  		dst.Components.Callbacks = make(map[string]*openapi3.CallbackRef)
    93  	}
    94  }
    95  
    96  func mergeComponents(dst, src *openapi3.T, replace bool) error {
    97  	if src.Components == nil {
    98  		return nil
    99  	}
   100  
   101  	if dst.Components == nil {
   102  		dst.Components = &openapi3.Components{}
   103  	}
   104  
   105  	initDestinationComponents(dst, src)
   106  
   107  	err := mergeMap(dst.Components.Schemas, src.Components.Schemas, replace)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	err = mergeMap(dst.Components.Parameters, src.Components.Parameters, replace)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	err = mergeMap(dst.Components.Headers, src.Components.Headers, replace)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	err = mergeMap(dst.Components.RequestBodies, src.Components.RequestBodies, replace)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	err = mergeMap(dst.Components.Responses, src.Components.Responses, replace)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	err = mergeMap(dst.Components.SecuritySchemes, src.Components.SecuritySchemes, replace)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	err = mergeMap(dst.Components.Examples, src.Components.Examples, replace)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	err = mergeMap(dst.Components.Links, src.Components.Links, replace)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	return mergeMap(dst.Components.Callbacks, src.Components.Callbacks, replace)
   140  }
   141  
   142  func mergeMap[T any](dst, src map[string]T, replace bool) error {
   143  	for k, v := range src {
   144  		existing, exists := dst[k]
   145  		if exists && !replace && !componentsEqual(v, existing) {
   146  			return errors.New("conflicting component: " + k)
   147  		}
   148  		dst[k] = v
   149  	}
   150  	return nil
   151  }
   152  
   153  func mergeExtensions(dst, src *openapi3.T, replace bool) {
   154  	if src.Extensions != nil && dst.Extensions == nil {
   155  		dst.Extensions = make(map[string]interface{}, len(src.Extensions))
   156  	}
   157  
   158  	for k, v := range src.Extensions {
   159  		// It's possible for specs to be merged from multiple stabilities and
   160  		// we don't want these different stability inputs to override
   161  		// the declared stability of the output we're building.
   162  		if k == ExtSnykApiStability {
   163  			continue
   164  		}
   165  
   166  		if _, ok := dst.Extensions[k]; !ok || replace {
   167  			dst.Extensions[k] = v
   168  		}
   169  	}
   170  }
   171  
   172  func mergeInfo(dst, src *openapi3.T, replace bool) {
   173  	if src.Info != nil && (dst.Info == nil || replace) {
   174  		dst.Info = src.Info
   175  	}
   176  }
   177  
   178  func mergePaths(dst, src *openapi3.T, replace bool) {
   179  	if src.Paths != nil && dst.Paths == nil {
   180  		dst.Paths = make(openapi3.Paths, len(src.Paths))
   181  	}
   182  	for k, v := range src.Paths {
   183  		if _, ok := dst.Paths[k]; !ok || replace {
   184  			dst.Paths[k] = v
   185  		}
   186  	}
   187  }
   188  
   189  func mergeSecurityRequirements(dst, src *openapi3.T, replace bool) {
   190  	if len(src.Security) > 0 && (len(dst.Security) == 0 || replace) {
   191  		dst.Security = src.Security
   192  	}
   193  }
   194  
   195  func mergeServers(dst, src *openapi3.T, replace bool) {
   196  	if len(src.Servers) > 0 && (len(dst.Security) == 0 || replace) {
   197  		dst.Servers = src.Servers
   198  	}
   199  }