1package loads
2
3import (
4	"encoding/json"
5	"errors"
6	"net/url"
7
8	"github.com/go-openapi/spec"
9	"github.com/go-openapi/swag"
10)
11
12var (
13	// Default chain of loaders, defined at the package level.
14	//
15	// By default this matches json and yaml documents.
16	//
17	// May be altered with AddLoader().
18	loaders *loader
19)
20
21func init() {
22	jsonLoader := &loader{
23		DocLoaderWithMatch: DocLoaderWithMatch{
24			Match: func(pth string) bool {
25				return true
26			},
27			Fn: JSONDoc,
28		},
29	}
30
31	loaders = jsonLoader.WithHead(&loader{
32		DocLoaderWithMatch: DocLoaderWithMatch{
33			Match: swag.YAMLMatcher,
34			Fn:    swag.YAMLDoc,
35		},
36	})
37
38	// sets the global default loader for go-openapi/spec
39	spec.PathLoader = loaders.Load
40}
41
42// DocLoader represents a doc loader type
43type DocLoader func(string) (json.RawMessage, error)
44
45// DocMatcher represents a predicate to check if a loader matches
46type DocMatcher func(string) bool
47
48// DocLoaderWithMatch describes a loading function for a given extension match.
49type DocLoaderWithMatch struct {
50	Fn    DocLoader
51	Match DocMatcher
52}
53
54// NewDocLoaderWithMatch builds a DocLoaderWithMatch to be used in load options
55func NewDocLoaderWithMatch(fn DocLoader, matcher DocMatcher) DocLoaderWithMatch {
56	return DocLoaderWithMatch{
57		Fn:    fn,
58		Match: matcher,
59	}
60}
61
62type loader struct {
63	DocLoaderWithMatch
64	Next *loader
65}
66
67// WithHead adds a loader at the head of the current stack
68func (l *loader) WithHead(head *loader) *loader {
69	if head == nil {
70		return l
71	}
72	head.Next = l
73	return head
74}
75
76// WithNext adds a loader at the trail of the current stack
77func (l *loader) WithNext(next *loader) *loader {
78	l.Next = next
79	return next
80}
81
82// Load the raw document from path
83func (l *loader) Load(path string) (json.RawMessage, error) {
84	_, erp := url.Parse(path)
85	if erp != nil {
86		return nil, erp
87	}
88
89	var lastErr error = errors.New("no loader matched") // default error if no match was found
90	for ldr := l; ldr != nil; ldr = ldr.Next {
91		if ldr.Match != nil && !ldr.Match(path) {
92			continue
93		}
94
95		// try then move to next one if there is an error
96		b, err := ldr.Fn(path)
97		if err == nil {
98			return b, nil
99		}
100
101		lastErr = err
102	}
103
104	return nil, lastErr
105}
106
107// JSONDoc loads a json document from either a file or a remote url
108func JSONDoc(path string) (json.RawMessage, error) {
109	data, err := swag.LoadFromFileOrHTTP(path)
110	if err != nil {
111		return nil, err
112	}
113	return json.RawMessage(data), nil
114}
115
116// AddLoader for a document, executed before other previously set loaders.
117//
118// This sets the configuration at the package level.
119//
120// NOTE:
121//  * this updates the default loader used by github.com/go-openapi/spec
122//  * since this sets package level globals, you shouln't call this concurrently
123//
124func AddLoader(predicate DocMatcher, load DocLoader) {
125	loaders = loaders.WithHead(&loader{
126		DocLoaderWithMatch: DocLoaderWithMatch{
127			Match: predicate,
128			Fn:    load,
129		},
130	})
131
132	// sets the global default loader for go-openapi/spec
133	spec.PathLoader = loaders.Load
134}
135