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

     1  package vervet
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net/url"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/getkin/kin-openapi/openapi3"
    13  	"github.com/ghodss/yaml"
    14  	"github.com/google/uuid"
    15  )
    16  
    17  func init() {
    18  	// Necessary for `format: uuid` to validate.
    19  	openapi3.DefineStringFormatCallback("uuid", func(v string) error {
    20  		_, err := uuid.Parse(v)
    21  		return err
    22  	})
    23  	openapi3.DefineStringFormatCallback("url", func(v string) error {
    24  		_, err := url.Parse(v)
    25  		return err
    26  	})
    27  }
    28  
    29  // Document is an OpenAPI 3 document object model.
    30  type Document struct {
    31  	*openapi3.T
    32  	path string
    33  	url  *url.URL
    34  }
    35  
    36  // NewDocumentFile loads an OpenAPI spec file from the given file path,
    37  // returning a document object.
    38  func NewDocumentFile(specFile string) (_ *Document, returnErr error) {
    39  	// Restore current working directory upon returning
    40  	cwd, err := os.Getwd()
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	defer func() {
    45  		err := os.Chdir(cwd)
    46  		if err != nil {
    47  			log.Println("warning: failed to restore working directory: %w", err)
    48  			if returnErr == nil {
    49  				returnErr = err
    50  			}
    51  		}
    52  	}()
    53  
    54  	specFile, err = filepath.Abs(specFile)
    55  	if err != nil {
    56  		return nil, fmt.Errorf("failed to get absolute path: %w", err)
    57  	}
    58  
    59  	// `cd` to the path containing the spec file, so that relative paths
    60  	// resolve.
    61  	specBase, specDir := filepath.Base(specFile), filepath.Dir(specFile)
    62  	err = os.Chdir(specDir)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("failed to chdir %q: %w", specDir, err)
    65  	}
    66  
    67  	specURL, err := url.Parse(specFile)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	var t openapi3.T
    73  	contents, err := os.ReadFile(specFile)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	err = yaml.Unmarshal(contents, &t)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	err = newRefAliasResolver(&t).resolve()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	l := openapi3.NewLoader()
    87  	l.IsExternalRefsAllowed = true
    88  	err = l.ResolveRefsIn(&t, specURL)
    89  	if err != nil {
    90  		return nil, fmt.Errorf("failed to load %q: %w", specBase, err)
    91  	}
    92  
    93  	if t.Components == nil {
    94  		t.Components = &openapi3.Components{}
    95  	}
    96  
    97  	return &Document{
    98  		T:    &t,
    99  		path: specFile,
   100  		url:  specURL,
   101  	}, nil
   102  }
   103  
   104  // NewResolvedDocument returns a Document that has already been loaded and
   105  // references resolved from the given URL. The URL is provided to indicate the
   106  // document's origin in logging and error messages.
   107  func NewResolvedDocument(t *openapi3.T, url *url.URL) *Document {
   108  	return &Document{
   109  		T:    t,
   110  		path: url.String(),
   111  		url:  url,
   112  	}
   113  }
   114  
   115  // MarshalJSON implements json.Marshaler.
   116  func (d *Document) MarshalJSON() ([]byte, error) {
   117  	return d.T.MarshalJSON()
   118  }
   119  
   120  // RelativePath returns the relative path for resolving references from the
   121  // file path location of the top-level document: the directory which contains
   122  // the file from which the top-level document was loaded.
   123  func (d *Document) RelativePath() string {
   124  	return filepath.Dir(d.path)
   125  }
   126  
   127  // Location returns the URL from where the document was loaded.
   128  func (d *Document) Location() *url.URL {
   129  	return d.url
   130  }
   131  
   132  // ResolveRefs resolves all Ref types in the document, causing the Value field
   133  // of each Ref to be loaded and populated from its referenced location.
   134  func (d *Document) ResolveRefs() error {
   135  	l := openapi3.NewLoader()
   136  	l.IsExternalRefsAllowed = true
   137  	return l.ResolveRefsIn(d.T, d.url)
   138  }
   139  
   140  // LoadReference loads a reference from refPath, relative to relPath, into
   141  // target. The relative path of the reference is returned, so that references
   142  // may be chain-loaded with successive calls.
   143  func (d *Document) LoadReference(relPath, refPath string, target interface{}) (_ string, returnErr error) {
   144  	refUrl, err := url.Parse(refPath)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  	if refUrl.Scheme == "" || refUrl.Scheme == "file" {
   149  		refPath, err = filepath.Abs(filepath.Join(relPath, refUrl.Path))
   150  		if err != nil {
   151  			return "", err
   152  		}
   153  		refUrl.Path = refPath
   154  	}
   155  
   156  	// Parse and load the contents of the referenced document.
   157  	l := openapi3.NewLoader()
   158  	l.IsExternalRefsAllowed = true
   159  	contents, err := openapi3.DefaultReadFromURI(l, refUrl)
   160  	if err != nil {
   161  		return "", fmt.Errorf("failed to read %q: %w", refUrl, err)
   162  	}
   163  	// If the reference is to an element in the referenced document, further resolve that.
   164  	if refUrl.Fragment != "" {
   165  		parts := strings.Split(refUrl.Fragment, "/")
   166  		// TODO: support actual jsonpaths if/when needed. For now only
   167  		// top-level properties are supported.
   168  		if parts[0] != "" || len(parts) > 2 {
   169  			return "", fmt.Errorf("URL %q not supported", refUrl)
   170  		}
   171  		elements := map[string]interface{}{}
   172  		err := yaml.Unmarshal(contents, &elements)
   173  		if err != nil {
   174  			return "", err
   175  		}
   176  		elementDoc, ok := elements[parts[1]]
   177  		if !ok {
   178  			return "", fmt.Errorf("element %q not found in %q", parts[1], refUrl.Path)
   179  		}
   180  		contents, err = json.Marshal(elementDoc)
   181  		if err != nil {
   182  			return "", err
   183  		}
   184  	}
   185  
   186  	// Unmarshal the resolved reference into target object.
   187  	err = yaml.Unmarshal(contents, target)
   188  	if err != nil {
   189  		return "", err
   190  	}
   191  
   192  	return filepath.Abs(filepath.Dir(refUrl.Path))
   193  }
   194  
   195  // Version returns the version of the document.
   196  func (d *Document) Version() (Version, error) {
   197  	vs, err := ExtensionString(d.Extensions, ExtSnykApiVersion)
   198  	if err != nil {
   199  		return Version{}, err
   200  	}
   201  	return ParseVersion(vs)
   202  }
   203  
   204  // Lifecycle returns the lifecycle of the document.
   205  func (d *Document) Lifecycle() (Lifecycle, error) {
   206  	ls, err := ExtensionString(d.Extensions, ExtSnykApiLifecycle)
   207  	if err != nil {
   208  		if IsExtensionNotFound(err) {
   209  			// If it's not marked as deprecated or sunset, assume it's released.
   210  			return LifecycleReleased, err
   211  		}
   212  		return lifecycleUndefined, err
   213  	}
   214  	return ParseLifecycle(ls)
   215  }