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