github.com/jasonfriedland/openapi2proto@v0.2.1/openapi/openapi.go (about) 1 // Package openapi contains tools to read in OpenAPI specifications 2 // so that they can be passed to the openapi2proto compiler 3 package openapi 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/pkg/errors" 17 yaml "gopkg.in/yaml.v2" 18 ) 19 20 func fetchRemoteContent(u string) (io.Reader, error) { 21 res, err := http.Get(u) 22 if err != nil { 23 return nil, errors.Wrap(err, `failed to get remote content`) 24 } 25 26 if res.StatusCode != http.StatusOK { 27 return nil, errors.Errorf(`remote content responded with status %d`, res.StatusCode) 28 } 29 30 defer res.Body.Close() 31 32 var buf bytes.Buffer 33 if _, err := io.Copy(&buf, res.Body); err != nil { 34 return nil, errors.Wrap(err, `failed to read remote content`) 35 } 36 37 return &buf, nil 38 } 39 40 // LoadFile loads an OpenAPI spec from a file, or a remote HTTP(s) location. 41 // This function also resolves any external references. 42 func LoadFile(fn string) (*Spec, error) { 43 var src io.Reader 44 var options []Option 45 if u, err := url.Parse(fn); err == nil && (u.Scheme == `http` || u.Scheme == `https`) { 46 rdr, err := fetchRemoteContent(u.String()) 47 if err != nil { 48 return nil, errors.Wrapf(err, `failed to fetch remote content %s`, fn) 49 } 50 src = rdr 51 } else { 52 f, err := os.Open(fn) 53 if err != nil { 54 return nil, errors.Wrapf(err, `failed to open file %s`, fn) 55 } 56 defer f.Close() 57 src = f 58 options = append(options, WithDir(filepath.Dir(fn))) 59 } 60 61 // from the file name, guess how we can decode this 62 var v interface{} 63 switch ext := strings.ToLower(path.Ext(fn)); ext { 64 case ".yaml", ".yml": 65 if err := yaml.NewDecoder(src).Decode(&v); err != nil { 66 return nil, errors.Wrapf(err, `failed to decode file %s`, fn) 67 } 68 case ".json": 69 if err := json.NewDecoder(src).Decode(&v); err != nil { 70 return nil, errors.Wrapf(err, `failed to decode file %s`, fn) 71 } 72 default: 73 return nil, errors.Errorf(`unsupported file extension type %s`, ext) 74 } 75 76 resolved, err := newResolver().Resolve(v, options...) 77 if err != nil { 78 return nil, errors.Wrap(err, `failed to resolve external references`) 79 } 80 81 // We re-encode the structure here because ... it's easier this way. 82 // 83 // One way to resolve references is to create an openapi.Spec structure 84 // populated with the values from the spec file, and when traverse the 85 // tree and resolve references as we compile this data into protobuf.* 86 // 87 // But when we do resolve a reference -- an external reference, in 88 // particular -- we must be aware of the context in which this piece of 89 // data is being compiled in. For example, compiling parameters is 90 // different from compiling responses. It's also usually the caller 91 // that knows the context of the compilation, not the current method 92 // that is resolving the reference. So in order for the method that 93 // is resolving the reference to know what to do, it must know the context 94 // in which it is being compiled. This means that we need to pass 95 // several bits of hints down the call chain to invokve the correct 96 // processing. But that comes with more complicated code (hey, I know 97 // the code is already complicated enough -- I mean, *more* complicated 98 // code, ok?) 99 // 100 // One way we tackle this is to resolve references in a separate pass 101 // than the main compilation. We actually do this in compiler.compileParameters, 102 // and compiler.compileDefinitions, which pre-compiles #/parameters/* 103 // and #/definitions/* so that when we encounter references, all we need 104 // to do is to fetch that pre-compiled piece of data and inject accordingly. 105 // 106 // This allows the compilation phase to treat internal references as just 107 // aliases to pre-generated data, but if we do this to external references, 108 // we need to include the steps to fetch, find the context, and compile the 109 // data during the main compile phase, which is not pretty. 110 // 111 // We would almost like to do the same thing for external references, but 112 // the thing with external references is that we can't pre-compile them 113 // based on where they are inserted, because they could potentially insert 114 // bits of completely unregulated data -- for example, if we knew that 115 // external references can only populate #/parameter it would be simple, 116 // but `$ref`s can creep up anywhere, and it's extremely hard to switch 117 // the code to be called based on this context. 118 // 119 // So instead of trying hard, to figure out what we were doing when 120 // we are resolving external references, we just inject the fetched 121 // data blindly into the structure, and re-encode it to look like 122 // that was the initial data -- after re-encoding, we can just treat 123 // the data as a complete, self-contained spec. Bad data will be 124 // weeded out during the deserialization phase, and we know exactly 125 // what we are doing when we are traversing the openapi spec. 126 var buf bytes.Buffer 127 if err := json.NewEncoder(&buf).Encode(resolved); err != nil { 128 return nil, errors.Wrap(err, `failed to encode resolved schema`) 129 } 130 131 var spec Spec 132 if err := json.Unmarshal(buf.Bytes(), &spec); err != nil { 133 return nil, errors.Wrap(err, `failed to decode content`) 134 } 135 136 // One last thing: populate some fields that are obvious to 137 // human beings, but required for dumb computers to process 138 // efficiently 139 for path, p := range spec.Paths { 140 if v := p.Get; v != nil { 141 v.Verb = "get" 142 v.Path = path 143 } 144 if v := p.Put; v != nil { 145 v.Verb = "put" 146 v.Path = path 147 } 148 if v := p.Post; v != nil { 149 v.Verb = "post" 150 v.Path = path 151 } 152 if v := p.Delete; v != nil { 153 v.Verb = "delete" 154 v.Path = path 155 } 156 } 157 158 return &spec, nil 159 }