seahorse 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +12 -0
- data/README.md +3 -0
- data/Rakefile +7 -0
- data/lib/seahorse.rb +15 -0
- data/lib/seahorse/api_translator/inflector.rb +37 -0
- data/lib/seahorse/api_translator/operation.rb +150 -0
- data/lib/seahorse/api_translator/shape.rb +235 -0
- data/lib/seahorse/controller.rb +87 -0
- data/lib/seahorse/model.rb +82 -0
- data/lib/seahorse/operation.rb +66 -0
- data/lib/seahorse/param_validator.rb +158 -0
- data/lib/seahorse/railtie.rb +7 -0
- data/lib/seahorse/router.rb +20 -0
- data/lib/seahorse/shape_builder.rb +84 -0
- data/lib/seahorse/type.rb +220 -0
- data/lib/seahorse/version.rb +3 -0
- data/lib/tasks/seahorse_tasks.rake +24 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d97791002e0e789b3434e96537b6638aa4250099
|
4
|
+
data.tar.gz: 231d4abf711240a602e06dab1fc7b1a9a6c15f52
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c16d3d5420f6be5ad2ab1d4b511ae3e6f93c22311379c49e97e3f92f3219dbd862e990d066b9ef44188baceb3bc3ee60bdb60e9b07a81fd98acb8628341895ba
|
7
|
+
data.tar.gz: dc3f7d18cd934f2428da5312125d7050d6cf5bcf142ab198d3b8d9515da1d0a4373506f1a1d8fb7829a49394d4769ea274bce7c453b2e76f4af8d56db72e50e8
|
data/LICENSE
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
may not use this file except in compliance with the License. A copy of
|
5
|
+
the License is located at
|
6
|
+
|
7
|
+
http://aws.amazon.com/apache2.0/
|
8
|
+
|
9
|
+
or in the "license" file accompanying this file. This file is
|
10
|
+
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
ANY KIND, either express or implied. See the License for the specific
|
12
|
+
language governing permissions and limitations under the License.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/lib/seahorse.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
require 'seahorse/api_translator/operation'
|
5
|
+
require 'seahorse/api_translator/shape'
|
6
|
+
require 'seahorse/api_translator/inflector'
|
7
|
+
require 'seahorse/controller'
|
8
|
+
require 'seahorse/router'
|
9
|
+
require 'seahorse/model'
|
10
|
+
require 'seahorse/operation'
|
11
|
+
require 'seahorse/type'
|
12
|
+
require 'seahorse/shape_builder'
|
13
|
+
require 'seahorse/version'
|
14
|
+
|
15
|
+
require 'seahorse/railtie' if defined?(Rails)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
module Seahorse
|
15
|
+
class ApiTranslator
|
16
|
+
|
17
|
+
# @private
|
18
|
+
module Inflector
|
19
|
+
|
20
|
+
# Performs a very simple inflection on on the words as they are
|
21
|
+
# formatted in the source API configurations. These are *not*
|
22
|
+
# general case inflectors.
|
23
|
+
# @param [String] string The string to inflect.
|
24
|
+
# @param [String,nil] format Valid formats include 'snake_case',
|
25
|
+
# 'camelCase' and `nil` (leave as is).
|
26
|
+
# @return [String]
|
27
|
+
def inflect string, format = nil
|
28
|
+
case format
|
29
|
+
when 'camelCase' then string.camelize
|
30
|
+
when 'snake_case' then string.underscore
|
31
|
+
else string
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
require_relative './inflector'
|
15
|
+
require_relative './shape'
|
16
|
+
|
17
|
+
module Seahorse
|
18
|
+
class ApiTranslator
|
19
|
+
|
20
|
+
# @private
|
21
|
+
class Operation
|
22
|
+
|
23
|
+
include Inflector
|
24
|
+
|
25
|
+
def initialize rules, options = {}
|
26
|
+
@options = options
|
27
|
+
|
28
|
+
@method_name = rules['name'].sub(/\d{4}_\d{2}_\d{2}$/, '')
|
29
|
+
@method_name = inflect(@method_name, @options[:inflect_method_names])
|
30
|
+
|
31
|
+
@rules = rules
|
32
|
+
|
33
|
+
if @rules['http']
|
34
|
+
@rules['http'].delete('response_code')
|
35
|
+
end
|
36
|
+
|
37
|
+
translate_input
|
38
|
+
translate_output
|
39
|
+
|
40
|
+
if @options[:documentation]
|
41
|
+
@rules['errors'] = @rules['errors'].map {|e| e['shape_name'] }
|
42
|
+
else
|
43
|
+
@rules.delete('errors')
|
44
|
+
@rules.delete('documentation')
|
45
|
+
@rules.delete('documentation_url')
|
46
|
+
@rules.delete('response_code')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String]
|
51
|
+
attr_reader :method_name
|
52
|
+
|
53
|
+
# @return [Hash]
|
54
|
+
attr_reader :rules
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def translate_input
|
59
|
+
if @rules['input']
|
60
|
+
rules = InputShape.new(@rules['input'], @options).rules
|
61
|
+
rules['members'] ||= {}
|
62
|
+
rules = normalize_inputs(rules)
|
63
|
+
else
|
64
|
+
rules = {
|
65
|
+
'type' => 'structure',
|
66
|
+
'members' => {},
|
67
|
+
}
|
68
|
+
end
|
69
|
+
@rules['input'] = rules
|
70
|
+
end
|
71
|
+
|
72
|
+
def translate_output
|
73
|
+
if @rules['output']
|
74
|
+
rules = OutputShape.new(@rules['output'], @options).rules
|
75
|
+
move_up_outputs(rules)
|
76
|
+
cache_payload(rules)
|
77
|
+
else
|
78
|
+
rules = {
|
79
|
+
'type' => 'structure',
|
80
|
+
'members' => {},
|
81
|
+
}
|
82
|
+
end
|
83
|
+
@rules['output'] = rules
|
84
|
+
end
|
85
|
+
|
86
|
+
def normalize_inputs rules
|
87
|
+
return rules unless @options[:type].match(/rest/)
|
88
|
+
|
89
|
+
xml = @options[:type].match(/xml/)
|
90
|
+
payload = false
|
91
|
+
wrapper = false
|
92
|
+
|
93
|
+
if rules['members'].any?{|name,rule| rule['payload'] }
|
94
|
+
|
95
|
+
# exactly one member has the payload trait
|
96
|
+
payload, rule = rules['members'].find{|name,rule| rule['payload'] }
|
97
|
+
rule.delete('payload')
|
98
|
+
|
99
|
+
#if rule['type'] == 'structure'
|
100
|
+
# wrapper = payload
|
101
|
+
# payload = [payload]
|
102
|
+
#end
|
103
|
+
|
104
|
+
else
|
105
|
+
|
106
|
+
# no members marked themselves as the payload, collect everything
|
107
|
+
# without a location
|
108
|
+
payload = rules['members'].inject([]) do |list,(name,rule)|
|
109
|
+
list << name if !rule['location']
|
110
|
+
list
|
111
|
+
end
|
112
|
+
|
113
|
+
if payload.empty?
|
114
|
+
payload = false
|
115
|
+
elsif xml
|
116
|
+
wrapper = @rules['input']['shape_name']
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
rules = { 'wrapper' => wrapper }.merge(rules) if wrapper
|
122
|
+
rules = { 'payload' => payload }.merge(rules) if payload
|
123
|
+
rules
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
def move_up_outputs output
|
128
|
+
move_up = nil
|
129
|
+
(output['members'] || {}).each_pair do |member_name, rules|
|
130
|
+
if rules['payload'] and rules['type'] == 'structure'
|
131
|
+
rules.delete('payload')
|
132
|
+
move_up = member_name
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
if move_up
|
137
|
+
output['members'].merge!(output['members'].delete(move_up)['members'])
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def cache_payload rules
|
142
|
+
(rules['members'] || {}).each_pair do |member_name, rule|
|
143
|
+
rules['payload'] = member_name if rule['payload'] || rule['streaming']
|
144
|
+
rule.delete('payload')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
require_relative './inflector'
|
15
|
+
|
16
|
+
module Seahorse
|
17
|
+
class ApiTranslator
|
18
|
+
|
19
|
+
# @private
|
20
|
+
class Shape
|
21
|
+
|
22
|
+
include Inflector
|
23
|
+
|
24
|
+
def initialize rules, options = {}
|
25
|
+
@options = options
|
26
|
+
@rules = {}
|
27
|
+
@rules['name'] = options['name'] if options.key?('name')
|
28
|
+
set_type(rules.delete('type'))
|
29
|
+
rules.each_pair do |method,arg|
|
30
|
+
send("set_#{method}", *[arg])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def rules
|
35
|
+
if @rules['type'] != 'blob'
|
36
|
+
@rules
|
37
|
+
elsif @rules['payload'] or @rules['streaming']
|
38
|
+
@rules.merge('type' => 'binary')
|
39
|
+
else
|
40
|
+
@rules.merge('type' => 'base64')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def xmlname
|
45
|
+
if @rules['flattened']
|
46
|
+
(@rules['members'] || {})['name'] || @xmlname
|
47
|
+
else
|
48
|
+
@xmlname
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def set_timestamp_format format
|
55
|
+
@rules['format'] = format
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_type name
|
59
|
+
types = {
|
60
|
+
'structure' => 'structure',
|
61
|
+
'list' => 'list',
|
62
|
+
'map' => 'map',
|
63
|
+
'boolean' => 'boolean',
|
64
|
+
'timestamp' => 'timestamp',
|
65
|
+
'character' => 'string',
|
66
|
+
'double' => 'float',
|
67
|
+
'float' => 'float',
|
68
|
+
'integer' => 'integer',
|
69
|
+
'long' => 'integer',
|
70
|
+
'short' => 'integer',
|
71
|
+
'string' => 'string',
|
72
|
+
'blob' => 'blob',
|
73
|
+
'biginteger' => 'integer',
|
74
|
+
'bigdecimal' => 'float',
|
75
|
+
}
|
76
|
+
if name == 'string'
|
77
|
+
# Purposefully omitting type when string (to reduce size of the api
|
78
|
+
# configuration). The parsers use string as the default when
|
79
|
+
# 'type' is omitted.
|
80
|
+
#@rules['type'] = 'string'
|
81
|
+
elsif type = types[name]
|
82
|
+
@rules['type'] = type
|
83
|
+
else
|
84
|
+
raise "unhandled shape type: #{name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_members members
|
89
|
+
case @rules['type']
|
90
|
+
when 'structure'
|
91
|
+
@rules['members'] = {}
|
92
|
+
members.each_pair do |member_name,member_rules|
|
93
|
+
|
94
|
+
member_shape = new_shape(member_rules)
|
95
|
+
|
96
|
+
member_key = inflect(member_name, @options[:inflect_member_names])
|
97
|
+
member_rules = member_shape.rules
|
98
|
+
|
99
|
+
if member_name != member_key
|
100
|
+
member_rules = { 'name' => member_name }.merge(member_rules)
|
101
|
+
end
|
102
|
+
|
103
|
+
if swap_names?(member_shape)
|
104
|
+
member_rules['name'] = member_key
|
105
|
+
member_key = member_shape.xmlname
|
106
|
+
end
|
107
|
+
|
108
|
+
@rules['members'][member_key] = member_rules
|
109
|
+
|
110
|
+
end
|
111
|
+
when 'list'
|
112
|
+
@rules['members'] = new_shape(members).rules
|
113
|
+
when 'map'
|
114
|
+
@rules['members'] = new_shape(members).rules
|
115
|
+
else
|
116
|
+
raise "unhandled complex shape `#{@rules['type']}'"
|
117
|
+
end
|
118
|
+
@rules.delete('members') if @rules['members'].empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_keys rules
|
122
|
+
shape = new_shape(rules)
|
123
|
+
@rules['keys'] = shape.rules
|
124
|
+
@rules.delete('keys') if @rules['keys'].empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
def set_xmlname name
|
128
|
+
@xmlname = name
|
129
|
+
@rules['name'] = name
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_location location
|
133
|
+
@rules['location'] = (location == 'http_status' ? 'status' : location)
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_location_name header_name
|
137
|
+
@rules['name'] = header_name
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_payload state
|
141
|
+
@rules['payload'] = true if state
|
142
|
+
end
|
143
|
+
|
144
|
+
def set_flattened state
|
145
|
+
@rules['flattened'] = true if state
|
146
|
+
end
|
147
|
+
|
148
|
+
def set_streaming state
|
149
|
+
@rules['streaming'] = true if state
|
150
|
+
end
|
151
|
+
|
152
|
+
def set_xmlnamespace ns
|
153
|
+
@rules['xmlns'] = ns
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_xmlattribute state
|
157
|
+
@rules['attribute'] = true if state
|
158
|
+
end
|
159
|
+
|
160
|
+
def set_documentation docs
|
161
|
+
@rules['documentation'] = docs if @options[:documentation]
|
162
|
+
end
|
163
|
+
|
164
|
+
def set_enum values
|
165
|
+
@rules['enum'] = values if @options[:documentation]
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_wrapper state
|
169
|
+
@rules['wrapper'] = true if state
|
170
|
+
end
|
171
|
+
|
172
|
+
# we purposefully drop these, not useful unless you want to create
|
173
|
+
# static classes
|
174
|
+
def set_shape_name *args; end
|
175
|
+
def set_box *args; end
|
176
|
+
|
177
|
+
# @param [Hash] rules
|
178
|
+
# @option options [String] :name The name this shape has as a structure member.
|
179
|
+
def new_shape rules
|
180
|
+
self.class.new(rules, @options)
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
# @private
|
186
|
+
class InputShape < Shape
|
187
|
+
|
188
|
+
def set_required *args
|
189
|
+
@rules['required'] = true;
|
190
|
+
end
|
191
|
+
|
192
|
+
def set_member_order order
|
193
|
+
@rules['order'] = order
|
194
|
+
end
|
195
|
+
|
196
|
+
def set_min_length min
|
197
|
+
@rules['min_length'] = min if @options[:documentation]
|
198
|
+
end
|
199
|
+
|
200
|
+
def set_max_length max
|
201
|
+
@rules['max_length'] = max if @options[:documentation]
|
202
|
+
end
|
203
|
+
|
204
|
+
def set_pattern pattern
|
205
|
+
@rules['pattern'] = pattern if @options[:documentation]
|
206
|
+
end
|
207
|
+
|
208
|
+
def swap_names? shape
|
209
|
+
false
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
# @private
|
215
|
+
class OutputShape < Shape
|
216
|
+
|
217
|
+
# these traits are ignored for output shapes
|
218
|
+
def set_required *args; end
|
219
|
+
def set_member_order *args; end
|
220
|
+
def set_min_length *args; end
|
221
|
+
def set_max_length *args; end
|
222
|
+
def set_pattern *args; end
|
223
|
+
|
224
|
+
def swap_names? shape
|
225
|
+
if @options[:documentation]
|
226
|
+
false
|
227
|
+
else
|
228
|
+
!!(%w(query rest-xml).include?(@options[:type]) and shape.xmlname)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative './param_validator'
|
2
|
+
|
3
|
+
module Seahorse
|
4
|
+
module Controller
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
respond_to :json, :xml
|
9
|
+
|
10
|
+
rescue_from Exception, :with => :render_error
|
11
|
+
|
12
|
+
wrap_parameters false
|
13
|
+
|
14
|
+
before_filter do
|
15
|
+
@params = params
|
16
|
+
@params = operation.input.from_input(params, false)
|
17
|
+
@params.update(operation.input.from_input(map_headers, false))
|
18
|
+
|
19
|
+
begin
|
20
|
+
input_rules = operation.to_hash['input']
|
21
|
+
%w(action controller format).each {|v| params.delete(v) }
|
22
|
+
validator = Seahorse::ParamValidator.new(input_rules)
|
23
|
+
validator.validate!(params)
|
24
|
+
rescue ArgumentError => error
|
25
|
+
if request.headers['HTTP_USER_AGENT'] =~ /sdk|cli/
|
26
|
+
service_error(error, 'ValidationError')
|
27
|
+
else
|
28
|
+
raise(error)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@params = operation.input.from_input(@params)
|
33
|
+
@params = params.permit(*operation.input.to_strong_params)
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def render_error(exception)
|
42
|
+
service_error(exception, exception.class.name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def params
|
46
|
+
@params || super
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_with(model, opts = {})
|
50
|
+
opts[:location] = nil
|
51
|
+
if opts[:error]
|
52
|
+
opts[:status] = opts[:error]
|
53
|
+
super
|
54
|
+
else
|
55
|
+
super(operation.output.to_output(model), opts)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def operation
|
60
|
+
return @operation if @operation
|
61
|
+
@operation = api_model.operation_from_action(action_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def api_model
|
65
|
+
return @api_model if @api_model
|
66
|
+
@api_model = ('Api::' + controller_name.singularize.camelcase).constantize
|
67
|
+
end
|
68
|
+
|
69
|
+
def service_error(error, code = 'ServiceError', status = 400)
|
70
|
+
respond_with({ code: code, message: error.message }, error: status)
|
71
|
+
end
|
72
|
+
|
73
|
+
def map_headers
|
74
|
+
return @map_headers if @map_headers
|
75
|
+
@map_headers = {}
|
76
|
+
return @map_headers unless operation.input.default_type == 'structure'
|
77
|
+
operation.input.members.each do |name, member|
|
78
|
+
if member.header
|
79
|
+
hdr_name = member.header == true ? name : member.header
|
80
|
+
hdr_name = "HTTP_" + hdr_name.upcase.gsub('-', '_')
|
81
|
+
@map_headers[name] = request.headers[hdr_name]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@map_headers
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Seahorse
|
2
|
+
module Model
|
3
|
+
@@apis ||= {}
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def apis; @@apis end
|
7
|
+
|
8
|
+
def add_all_routes(router)
|
9
|
+
Dir.glob("#{Rails.root}/app/models/api/*.rb").each {|f| load f }
|
10
|
+
@@apis.values.each {|api| api.add_routes(router) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
@@apis[name.underscore.gsub(/_api$|^api\//, '')] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
attr_reader :operations
|
22
|
+
|
23
|
+
def model_name
|
24
|
+
name.underscore.gsub(/_api$|^api\//, '')
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_routes(router)
|
28
|
+
Seahorse::Router.new(self).add_routes(router)
|
29
|
+
end
|
30
|
+
|
31
|
+
def desc(text)
|
32
|
+
@desc = text
|
33
|
+
end
|
34
|
+
|
35
|
+
def operation(name, &block)
|
36
|
+
name, action = *operation_name_and_action(name)
|
37
|
+
@actions ||= {}
|
38
|
+
@operations ||= {}
|
39
|
+
@operations[name] = Operation.new(self, name, action, &block)
|
40
|
+
@operations[name].documentation = @desc
|
41
|
+
@actions[action] = @operations[name]
|
42
|
+
@desc = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def operation_from_action(action)
|
46
|
+
@actions ||= {}
|
47
|
+
@actions[action]
|
48
|
+
end
|
49
|
+
|
50
|
+
def type(name, &block)
|
51
|
+
supertype = 'structure'
|
52
|
+
name, supertype = *name.map {|k,v| [k, v] }.flatten if Hash === name
|
53
|
+
ShapeBuilder.type(name, supertype, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_hash
|
57
|
+
ops = @operations.inject({}) do |hash, (name, operation)|
|
58
|
+
hash[name.camelcase(:lower)] = operation.to_hash
|
59
|
+
hash
|
60
|
+
end
|
61
|
+
{'operations' => ops}
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def operation_name_and_action(name)
|
67
|
+
if Hash === name
|
68
|
+
name.to_a.first.map(&:to_s).reverse
|
69
|
+
else
|
70
|
+
case name.to_s
|
71
|
+
when 'index', 'list'
|
72
|
+
["list_#{model_name.pluralize}", 'index']
|
73
|
+
when 'show'
|
74
|
+
["get_#{model_name}", name.to_s]
|
75
|
+
else
|
76
|
+
["#{name}_#{model_name}", name.to_s]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Seahorse
|
2
|
+
class Operation
|
3
|
+
attr_reader :name, :verb, :action
|
4
|
+
attr_accessor :documentation
|
5
|
+
|
6
|
+
def initialize(controller, name, action = nil, &block)
|
7
|
+
@name = name.to_s
|
8
|
+
@action = action.to_s
|
9
|
+
@controller = controller
|
10
|
+
url_prefix = "/" + controller.model_name.pluralize
|
11
|
+
url_extra = nil
|
12
|
+
|
13
|
+
case action.to_s
|
14
|
+
when 'index'
|
15
|
+
@verb = 'get'
|
16
|
+
when 'show'
|
17
|
+
@verb = 'get'
|
18
|
+
url_extra = ':id'
|
19
|
+
when 'destroy', 'delete'
|
20
|
+
@verb = 'delete'
|
21
|
+
url_extra = ':id'
|
22
|
+
when 'create'
|
23
|
+
@verb = 'post'
|
24
|
+
when 'update'
|
25
|
+
@verb = 'put'
|
26
|
+
else
|
27
|
+
@verb = 'get'
|
28
|
+
url_extra = name.to_s
|
29
|
+
end
|
30
|
+
@url = url_prefix + (url_extra ? "/#{url_extra}" : "")
|
31
|
+
|
32
|
+
instance_eval(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def verb(verb = nil)
|
36
|
+
verb ? (@verb = verb) : @verb
|
37
|
+
end
|
38
|
+
|
39
|
+
def url(url = nil)
|
40
|
+
url ? (@url = url) : @url
|
41
|
+
end
|
42
|
+
|
43
|
+
def input(type = nil, &block)
|
44
|
+
@input ||= ShapeBuilder.type_class_for(type || 'structure').new
|
45
|
+
type || block ? ShapeBuilder.new(@input).build(&block) : @input
|
46
|
+
end
|
47
|
+
|
48
|
+
def output(type = nil, &block)
|
49
|
+
@output ||= ShapeBuilder.type_class_for(type || 'structure').new
|
50
|
+
type || block ? ShapeBuilder.new(@output).build(&block) : @output
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_hash
|
54
|
+
{
|
55
|
+
'name' => name.camelcase,
|
56
|
+
'http' => {
|
57
|
+
'uri' => url.gsub(/:(\w+)/, '{\1}'),
|
58
|
+
'method' => verb.upcase
|
59
|
+
},
|
60
|
+
'input' => input.to_hash,
|
61
|
+
'output' => output.to_hash,
|
62
|
+
'documentation' => documentation
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
require 'date'
|
15
|
+
require 'time'
|
16
|
+
require 'pathname'
|
17
|
+
|
18
|
+
module Seahorse
|
19
|
+
# @api private
|
20
|
+
class ParamValidator
|
21
|
+
|
22
|
+
# @param [Hash] rules
|
23
|
+
def initialize rules
|
24
|
+
@rules = (rules || {})['members'] || {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [Hash] params A hash of request params.
|
28
|
+
# @raise [ArgumentError] Raises an `ArgumentError` if any of the given
|
29
|
+
# request parameters are invalid.
|
30
|
+
# @return [Boolean] Returns `true` if the `params` are valid.
|
31
|
+
def validate! params
|
32
|
+
validate_structure(@rules, params || {})
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate_structure rules, params, context = "params"
|
39
|
+
# require params to be a hash
|
40
|
+
unless params.is_a?(Hash)
|
41
|
+
raise ArgumentError, "expected a hash for #{context}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# check for missing required params
|
45
|
+
rules.each_pair do |param_name, rule|
|
46
|
+
if rule['required']
|
47
|
+
unless params.key?(param_name) or params.key?(param_name.to_sym)
|
48
|
+
msg = "missing required option :#{param_name} in #{context}"
|
49
|
+
raise ArgumentError, msg
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# validate hash members
|
55
|
+
params.each_pair do |param_name, param_value|
|
56
|
+
if param_rules = rules[param_name.to_s]
|
57
|
+
member_context = "#{context}[#{param_name.inspect}]"
|
58
|
+
validate_member(param_rules, param_value, member_context)
|
59
|
+
else
|
60
|
+
msg = "unexpected option #{param_name.inspect} found in #{context}"
|
61
|
+
raise ArgumentError, msg
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_list rules, params, context
|
67
|
+
# require an array
|
68
|
+
unless params.is_a?(Array)
|
69
|
+
raise ArgumentError, "expected an array for #{context}"
|
70
|
+
end
|
71
|
+
# validate array members
|
72
|
+
params.each_with_index do |param_value,n|
|
73
|
+
validate_member(rules, param_value, context + "[#{n}]")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_map rules, params, context
|
78
|
+
# require params to be a hash
|
79
|
+
unless params.is_a?(Hash)
|
80
|
+
raise ArgumentError, "expected a hash for #{context}"
|
81
|
+
end
|
82
|
+
# validate hash keys and members
|
83
|
+
params.each_pair do |key,param_value|
|
84
|
+
unless key.is_a?(String)
|
85
|
+
msg = "expected hash keys for #{context} to be strings"
|
86
|
+
raise ArgumentError, msg
|
87
|
+
end
|
88
|
+
validate_member(rules, param_value, context + "[#{key.inspect}]")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_member rules, param, context
|
93
|
+
member_rules = rules['members'] || {}
|
94
|
+
case rules['type']
|
95
|
+
when 'structure' then validate_structure(member_rules, param, context)
|
96
|
+
when 'list' then validate_list(member_rules, param, context)
|
97
|
+
when 'map' then validate_map(member_rules, param, context)
|
98
|
+
else validate_scalar(rules, param, context)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_scalar rules, param, context
|
103
|
+
case rules['type']
|
104
|
+
when 'string', nil
|
105
|
+
unless param.respond_to?(:to_str)
|
106
|
+
raise ArgumentError, "expected #{context} to be a string"
|
107
|
+
end
|
108
|
+
when 'integer'
|
109
|
+
unless param.respond_to?(:to_int)
|
110
|
+
raise ArgumentError, "expected #{context} to be an integer"
|
111
|
+
end
|
112
|
+
when 'timestamp'
|
113
|
+
case param
|
114
|
+
when Time, DateTime, Date, Integer
|
115
|
+
when /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
|
116
|
+
else
|
117
|
+
msg = "expected #{context} to be a Time/DateTime/Date object, "
|
118
|
+
msg << "an integer or an iso8601 string"
|
119
|
+
raise ArgumentError, msg
|
120
|
+
end
|
121
|
+
when 'boolean'
|
122
|
+
unless [true,false].include?(param)
|
123
|
+
raise ArgumentError, "expected #{context} to be a boolean"
|
124
|
+
end
|
125
|
+
when 'float'
|
126
|
+
unless param.is_a?(Numeric)
|
127
|
+
raise ArgumentError, "expected #{context} to be a Numeric (float)"
|
128
|
+
end
|
129
|
+
when 'base64', 'binary'
|
130
|
+
unless
|
131
|
+
param.is_a?(String) or
|
132
|
+
(param.respond_to?(:read) and param.respond_to?(:rewind)) or
|
133
|
+
param.is_a?(Pathname)
|
134
|
+
then
|
135
|
+
msg = "expected #{context} to be a string, an IO object or a "
|
136
|
+
msg << "Pathname object"
|
137
|
+
raise ArgumentError, msg
|
138
|
+
end
|
139
|
+
else
|
140
|
+
raise ArgumentError, "unhandled type `#{rules['type']}' for #{context}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class << self
|
145
|
+
|
146
|
+
# @param [Hash] rules
|
147
|
+
# @param [Hash] params
|
148
|
+
# @raise [ArgumentError] Raises an `ArgumentError` when one or more
|
149
|
+
# of the request parameters are invalid.
|
150
|
+
# @return [Boolean] Returns `true` when params are valid.
|
151
|
+
def validate! rules, params
|
152
|
+
ParamValidator.new(rules).validate!(params)
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Seahorse
|
2
|
+
class Router
|
3
|
+
def initialize(model)
|
4
|
+
@model = model
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_routes(router)
|
8
|
+
operations = @model.operations
|
9
|
+
controller = @model.model_name.pluralize
|
10
|
+
operations.each do |name, operation|
|
11
|
+
router.match "/#{name}" => "#{controller}##{operation.action}",
|
12
|
+
defaults: { format: 'json' },
|
13
|
+
via: [:get, operation.verb.to_sym].uniq
|
14
|
+
router.match operation.url => "#{controller}##{operation.action}",
|
15
|
+
defaults: { format: 'json' },
|
16
|
+
via: operation.verb
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative './type'
|
2
|
+
|
3
|
+
module Seahorse
|
4
|
+
class ShapeBuilder
|
5
|
+
def self.build_default_types
|
6
|
+
hash = HashWithIndifferentAccess.new
|
7
|
+
hash.update string: [StringType, nil],
|
8
|
+
timestamp: [TimestampType, nil],
|
9
|
+
integer: [IntegerType, nil],
|
10
|
+
boolean: [BooleanType, nil],
|
11
|
+
list: [ListType, nil],
|
12
|
+
structure: [StructureType, nil]
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.type(type, supertype = 'structure', &block)
|
17
|
+
klass = Class.new(type_class_for(supertype))
|
18
|
+
klass.type = type
|
19
|
+
@@types[type] = [klass, block]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.type_class_for(type)
|
23
|
+
@@types[type] ? @@types[type][0] : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(context)
|
27
|
+
@context = context
|
28
|
+
@desc = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def build(&block)
|
32
|
+
init_blocks = []
|
33
|
+
init_blocks << block if block_given?
|
34
|
+
|
35
|
+
# collect the init block for this type and all of its super types
|
36
|
+
klass = @context.class
|
37
|
+
while klass != Type
|
38
|
+
if block = @@types[klass.type][1]
|
39
|
+
init_blocks << block
|
40
|
+
end
|
41
|
+
klass = klass.superclass
|
42
|
+
end
|
43
|
+
|
44
|
+
init_blocks.reverse.each do |init_block|
|
45
|
+
instance_eval(&init_block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(type, *args, &block)
|
50
|
+
if @@types[type]
|
51
|
+
send_type(type, *args, &block)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def desc(text)
|
58
|
+
@desc = text
|
59
|
+
end
|
60
|
+
|
61
|
+
def model(model)
|
62
|
+
@context.model = model
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def send_type(type, *args, &block)
|
68
|
+
klass, init_block = *@@types[type.to_s]
|
69
|
+
shape = klass.new(*args)
|
70
|
+
shape.documentation = @desc
|
71
|
+
@context.add(shape)
|
72
|
+
if init_block || block
|
73
|
+
old_context, @context = @context, shape
|
74
|
+
instance_eval(&init_block) if init_block
|
75
|
+
instance_eval(&block) if block
|
76
|
+
@context = old_context
|
77
|
+
end
|
78
|
+
@desc = nil
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
@@types = build_default_types
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Seahorse
|
2
|
+
class Type
|
3
|
+
attr_accessor :name, :required, :model, :location, :header, :uri, :as
|
4
|
+
attr_accessor :documentation
|
5
|
+
|
6
|
+
def self.type; @type || name.to_s.underscore.gsub(/_type$|^.+\//, '') end
|
7
|
+
def self.type=(v) @type = v end
|
8
|
+
def self.inspect; "Type(#{type})" end
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
name, opts = nil, {}
|
12
|
+
if args.size == 0
|
13
|
+
name = type
|
14
|
+
elsif args.size == 2
|
15
|
+
name, opts = args.first, args.last
|
16
|
+
elsif Hash === args.first
|
17
|
+
opts = args.first
|
18
|
+
else
|
19
|
+
name = args.first
|
20
|
+
end
|
21
|
+
|
22
|
+
self.name = name.to_s
|
23
|
+
opts.each {|k, v| send("#{k}=", v) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect
|
27
|
+
variables = instance_variables.map do |v|
|
28
|
+
next if v.to_s =~ /^@(?:(?:default_)?type|name|model)$/
|
29
|
+
[v.to_s[1..-1], instance_variable_get(v).inspect].join('=')
|
30
|
+
end.compact.join(' ')
|
31
|
+
variables = ' ' + variables if variables.length > 0
|
32
|
+
"#<Type(#{type})#{variables}>"
|
33
|
+
end
|
34
|
+
|
35
|
+
def type
|
36
|
+
@type ||= self.class.type.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_type
|
40
|
+
klass = self.class
|
41
|
+
last_klass = nil
|
42
|
+
while klass != Type
|
43
|
+
last_klass = klass
|
44
|
+
klass = klass.superclass
|
45
|
+
end
|
46
|
+
@default_type ||= last_klass.type
|
47
|
+
end
|
48
|
+
|
49
|
+
def complex?; false end
|
50
|
+
|
51
|
+
def add(shape)
|
52
|
+
raise NotImplementedError, "Cannot add #{shape.inspect} to #{type}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_hash
|
56
|
+
hash = {'type' => default_type}
|
57
|
+
hash['required'] = true if required
|
58
|
+
hash['location'] = location if location
|
59
|
+
hash['location'] = 'uri' if uri
|
60
|
+
if header
|
61
|
+
header_name = header == true ? name : header
|
62
|
+
hash['location'] = 'header'
|
63
|
+
hash['location_name'] = header_name
|
64
|
+
hash['name'] = header_name
|
65
|
+
end
|
66
|
+
hash['documentation'] = documentation if documentation
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
|
70
|
+
def from_input(data, filter = true) data end
|
71
|
+
def to_output(data) pull_value(data).to_s end
|
72
|
+
def to_strong_params; name.to_s end
|
73
|
+
|
74
|
+
def pull_value(value)
|
75
|
+
found = false
|
76
|
+
names = as ? as : name
|
77
|
+
if names
|
78
|
+
names = [names].flatten
|
79
|
+
names.each do |name|
|
80
|
+
if value.respond_to?(name)
|
81
|
+
value = value.send(name)
|
82
|
+
found = true
|
83
|
+
elsif Hash === value
|
84
|
+
if value.has_key?(name)
|
85
|
+
value = value[name]
|
86
|
+
found = true
|
87
|
+
end
|
88
|
+
else
|
89
|
+
raise ArgumentError, "no property `#{name}' while looking for " +
|
90
|
+
"`#{names.join('.')}' on #{value.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
found ? value : nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class StringType < Type
|
99
|
+
def from_input(data, filter = true) data.to_s end
|
100
|
+
end
|
101
|
+
|
102
|
+
class TimestampType < Type
|
103
|
+
def from_input(data, filter = true)
|
104
|
+
String === data ? Time.parse(data) : data
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class IntegerType < Type
|
109
|
+
def from_input(data, filter = true) Integer(data) end
|
110
|
+
def to_output(data) (value = pull_value(data)) ? value.to_i : nil end
|
111
|
+
end
|
112
|
+
|
113
|
+
class BooleanType < Type
|
114
|
+
def from_input(data, filter = true) !!pull_value(data) end
|
115
|
+
end
|
116
|
+
|
117
|
+
class ListType < Type
|
118
|
+
attr_accessor :collection
|
119
|
+
|
120
|
+
def initialize(*args)
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
def complex?; true end
|
125
|
+
|
126
|
+
def add(shape)
|
127
|
+
self.collection = shape
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_hash
|
131
|
+
hash = super
|
132
|
+
hash['members'] = collection ? collection.to_hash : {}
|
133
|
+
hash
|
134
|
+
end
|
135
|
+
|
136
|
+
def from_input(data, filter = true)
|
137
|
+
data.each_with_index {|v, i| data[i] = collection.from_input(v, filter) }
|
138
|
+
data
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_output(data)
|
142
|
+
pull_value(data).map {|v| collection.to_output(v) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def to_strong_params
|
146
|
+
collection.complex? ? collection.to_strong_params : []
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class StructureType < Type
|
151
|
+
attr_accessor :members
|
152
|
+
|
153
|
+
def initialize(*args)
|
154
|
+
@members = {}
|
155
|
+
super
|
156
|
+
end
|
157
|
+
|
158
|
+
def complex?; true end
|
159
|
+
|
160
|
+
def add(shape)
|
161
|
+
members[shape.name.to_s] = shape
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_hash
|
165
|
+
hash = super
|
166
|
+
hash['members'] = members.inject({}) do |hsh, (k, v)|
|
167
|
+
hsh[k.to_s] = v.to_hash
|
168
|
+
hsh
|
169
|
+
end
|
170
|
+
hash
|
171
|
+
end
|
172
|
+
|
173
|
+
def from_input(data, filter = true)
|
174
|
+
return nil unless data
|
175
|
+
data.dup.each do |name, value|
|
176
|
+
if members[name]
|
177
|
+
if filter && members[name].type == 'list' && members[name].collection.model &&
|
178
|
+
ActiveRecord::Base > members[name].collection.model
|
179
|
+
then
|
180
|
+
data.delete(name)
|
181
|
+
data[name + '_attributes'] = members[name].from_input(value, filter)
|
182
|
+
else
|
183
|
+
data[name] = members[name].from_input(value, filter)
|
184
|
+
end
|
185
|
+
elsif filter
|
186
|
+
data.delete(name)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
data
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_output(data)
|
193
|
+
if Hash === data
|
194
|
+
data = data.with_indifferent_access unless HashWithIndifferentAccess === data
|
195
|
+
end
|
196
|
+
|
197
|
+
members.inject({}) do |hsh, (name, member)|
|
198
|
+
value = member.to_output(data)
|
199
|
+
hsh[name] = value if value
|
200
|
+
hsh
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def to_strong_params
|
205
|
+
members.map do |name, member|
|
206
|
+
if member.complex?
|
207
|
+
if member.type == 'list' && member.collection.model &&
|
208
|
+
ActiveRecord::Base > member.collection.model
|
209
|
+
then
|
210
|
+
name += '_attributes'
|
211
|
+
end
|
212
|
+
|
213
|
+
{name => member.to_strong_params}
|
214
|
+
else
|
215
|
+
name
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'oj'
|
2
|
+
|
3
|
+
namespace :seahorse do
|
4
|
+
desc 'Builds API clients'
|
5
|
+
task :api => :environment do
|
6
|
+
filename = 'service.json'
|
7
|
+
service = {
|
8
|
+
'format' => 'rest-json',
|
9
|
+
'type' => 'rest-json',
|
10
|
+
'endpoint_prefix' => '',
|
11
|
+
'operations' => {}
|
12
|
+
}
|
13
|
+
|
14
|
+
Dir.glob("#{Rails.root}/app/models/api/*.rb").each {|f| load f }
|
15
|
+
Seahorse::Model.apis.values.each do |api|
|
16
|
+
service['operations'].update(api.to_hash['operations'])
|
17
|
+
end
|
18
|
+
|
19
|
+
File.open(filename, 'w') do |f|
|
20
|
+
f.puts(Oj.dump(service, indent: 2))
|
21
|
+
end
|
22
|
+
puts "Wrote service description: #{filename}"
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: seahorse
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Loren Segal
|
8
|
+
- Trevor Rowe
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: oj
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
description: Easy web service descriptions
|
43
|
+
email:
|
44
|
+
- amazon@amazon.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- lib/seahorse/api_translator/inflector.rb
|
50
|
+
- lib/seahorse/api_translator/operation.rb
|
51
|
+
- lib/seahorse/api_translator/shape.rb
|
52
|
+
- lib/seahorse/controller.rb
|
53
|
+
- lib/seahorse/model.rb
|
54
|
+
- lib/seahorse/operation.rb
|
55
|
+
- lib/seahorse/param_validator.rb
|
56
|
+
- lib/seahorse/railtie.rb
|
57
|
+
- lib/seahorse/router.rb
|
58
|
+
- lib/seahorse/shape_builder.rb
|
59
|
+
- lib/seahorse/type.rb
|
60
|
+
- lib/seahorse/version.rb
|
61
|
+
- lib/seahorse.rb
|
62
|
+
- lib/tasks/seahorse_tasks.rake
|
63
|
+
- LICENSE
|
64
|
+
- Rakefile
|
65
|
+
- README.md
|
66
|
+
homepage: http://github.com/awslabs/seahorse
|
67
|
+
licenses: []
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.0.0
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: Seahorse is a way to describe web services
|
89
|
+
test_files: []
|
90
|
+
has_rdoc:
|