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 }