1// +build codegen
2
3package api
4
5import (
6	"encoding/json"
7	"fmt"
8	"os"
9	"path/filepath"
10	"sort"
11	"strings"
12)
13
14// APIs provides a set of API models loaded by API package name.
15type APIs map[string]*API
16
17// Loader provides the loading of APIs from files.
18type Loader struct {
19	// The base Go import path the loaded models will be appended to.
20	BaseImport string
21
22	// Allows ignoring API models that are unsupported by the SDK without
23	// failing the load of other supported APIs.
24	IgnoreUnsupportedAPIs bool
25}
26
27// Load loads the API model files from disk returning the map of API package.
28// Returns error if multiple API model resolve to the same package name.
29func (l Loader) Load(modelPaths []string) (APIs, error) {
30	apis := APIs{}
31	for _, modelPath := range modelPaths {
32		a, err := loadAPI(modelPath, l.BaseImport, func(a *API) {
33			a.IgnoreUnsupportedAPIs = l.IgnoreUnsupportedAPIs
34		})
35		if err != nil {
36			return nil, fmt.Errorf("failed to load API, %v, %v", modelPath, err)
37		}
38
39		if len(a.Operations) == 0 {
40			if l.IgnoreUnsupportedAPIs {
41				fmt.Fprintf(os.Stderr, "API has no operations, ignoring model %s, %v\n",
42					modelPath, a.ImportPath())
43				continue
44			}
45		}
46
47		importPath := a.ImportPath()
48		if _, ok := apis[importPath]; ok {
49			return nil, fmt.Errorf(
50				"package names must be unique attempted to load %v twice. Second model file: %v",
51				importPath, modelPath)
52		}
53		apis[importPath] = a
54	}
55
56	return apis, nil
57}
58
59// attempts to load a model from disk into the import specified. Additional API
60// options are invoked before to the API's Setup being called.
61func loadAPI(modelPath, baseImport string, opts ...func(*API)) (*API, error) {
62	a := &API{
63		BaseImportPath:   baseImport,
64		BaseCrosslinkURL: "https://docs.aws.amazon.com",
65	}
66
67	modelFile := filepath.Base(modelPath)
68	modelDir := filepath.Dir(modelPath)
69	err := attachModelFiles(modelDir,
70		modelLoader{modelFile, a.Attach, true},
71		modelLoader{"docs-2.json", a.AttachDocs, false},
72		modelLoader{"paginators-1.json", a.AttachPaginators, false},
73		modelLoader{"waiters-2.json", a.AttachWaiters, false},
74		modelLoader{"examples-1.json", a.AttachExamples, false},
75		modelLoader{"smoke.json", a.AttachSmokeTests, false},
76	)
77	if err != nil {
78		return nil, err
79	}
80
81	for _, opt := range opts {
82		opt(a)
83	}
84
85	if err = a.Setup(); err != nil {
86		return nil, err
87	}
88
89	return a, nil
90}
91
92type modelLoader struct {
93	Filename string
94	Loader   func(string) error
95	Required bool
96}
97
98func attachModelFiles(modelPath string, modelFiles ...modelLoader) error {
99	for _, m := range modelFiles {
100		filepath := filepath.Join(modelPath, m.Filename)
101		_, err := os.Stat(filepath)
102		if os.IsNotExist(err) && !m.Required {
103			continue
104		} else if err != nil {
105			return fmt.Errorf("failed to load model file %v, %v", m.Filename, err)
106		}
107
108		if err = m.Loader(filepath); err != nil {
109			return fmt.Errorf("model load failed, %s, %v", modelPath, err)
110		}
111	}
112
113	return nil
114}
115
116// ExpandModelGlobPath returns a slice of model paths expanded from the glob
117// pattern passed in. Returns the path of the model file to be loaded. Includes
118// all versions of a service model.
119//
120//   e.g:
121//   models/apis/*/*/api-2.json
122//
123//   Or with specific model file:
124//   models/apis/service/version/api-2.json
125func ExpandModelGlobPath(globs ...string) ([]string, error) {
126	modelPaths := []string{}
127
128	for _, g := range globs {
129		filepaths, err := filepath.Glob(g)
130		if err != nil {
131			return nil, err
132		}
133		for _, p := range filepaths {
134			modelPaths = append(modelPaths, p)
135		}
136	}
137
138	return modelPaths, nil
139}
140
141// TrimModelServiceVersions sorts the model paths by service version then
142// returns recent model versions, and model version excluded.
143//
144// Uses the third from last path element to determine unique service. Only one
145// service version will be included.
146//
147//   models/apis/service/version/api-2.json
148func TrimModelServiceVersions(modelPaths []string) (include, exclude []string) {
149	sort.Strings(modelPaths)
150
151	// Remove old API versions from list
152	m := map[string]struct{}{}
153	for i := len(modelPaths) - 1; i >= 0; i-- {
154		// service name is 2nd-to-last component
155		parts := strings.Split(modelPaths[i], string(filepath.Separator))
156		svc := parts[len(parts)-3]
157
158		if _, ok := m[svc]; ok {
159			// Removed unused service version
160			exclude = append(exclude, modelPaths[i])
161			continue
162		}
163		include = append(include, modelPaths[i])
164		m[svc] = struct{}{}
165	}
166
167	return include, exclude
168}
169
170// Attach opens a file by name, and unmarshal its JSON data.
171// Will proceed to setup the API if not already done so.
172func (a *API) Attach(filename string) error {
173	a.path = filepath.Dir(filename)
174	f, err := os.Open(filename)
175	if err != nil {
176		return err
177	}
178	defer f.Close()
179
180	if err := json.NewDecoder(f).Decode(a); err != nil {
181		return fmt.Errorf("failed to decode %s, err: %v", filename, err)
182	}
183
184	return nil
185}
186
187// AttachString will unmarshal a raw JSON string, and setup the
188// API if not already done so.
189func (a *API) AttachString(str string) error {
190	json.Unmarshal([]byte(str), a)
191
192	if a.initialized {
193		return nil
194	}
195
196	return a.Setup()
197}
198
199// Setup initializes the API.
200func (a *API) Setup() error {
201	a.setServiceAliaseName()
202	a.setMetadataEndpointsKey()
203	a.writeShapeNames()
204	a.resolveReferences()
205
206	if !a.NoRemoveUnusedShapes {
207		a.removeUnusedShapes()
208	}
209
210	a.fixStutterNames()
211	a.renameExportable()
212	a.applyShapeNameAliases()
213	a.createInputOutputShapes()
214	a.writeInputOutputLocationName()
215	a.renameAPIPayloadShapes()
216	a.renameCollidingFields()
217	a.updateTopLevelShapeReferences()
218	a.renameS3EventStreamMember()
219	if err := a.setupEventStreams(); err != nil {
220		return err
221	}
222
223	a.findEndpointDiscoveryOp()
224	a.injectUnboundedOutputStreaming()
225	a.customizationPasses()
226
227	if !a.NoRemoveUnusedShapes {
228		a.removeUnusedShapes()
229	}
230
231	if !a.NoValidataShapeMethods {
232		a.addShapeValidations()
233	}
234
235	a.initialized = true
236
237	return nil
238}
239
240// UnsupportedAPIModelError provides wrapping of an error causing the API to
241// fail to load because the SDK does not support the API service defined.
242type UnsupportedAPIModelError struct {
243	Err error
244}
245
246func (e UnsupportedAPIModelError) Error() string {
247	return fmt.Sprintf("service API is not supported, %v", e.Err)
248}
249