github.com/snyk/vervet/v3@v3.7.0/document.go (about)

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