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.
@@ -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: