seahorse 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|