seahorse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,3 @@
1
+ # Seahorse
2
+
3
+ This project rocks and uses Apache 2.0 License.
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ Bundler::GemHelper.install_tasks
@@ -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,7 @@
1
+ module Seahorse
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load File.dirname(__FILE__) + '/../tasks/seahorse_tasks.rake'
5
+ end
6
+ end
7
+ 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,3 @@
1
+ module Seahorse
2
+ VERSION = "0.1.0"
3
+ 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: