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  }