angus 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/bin/angus +13 -0
  2. data/lib/angus.rb +6 -4
  3. data/lib/angus/base.rb +119 -0
  4. data/lib/angus/base_actions.rb +38 -0
  5. data/lib/angus/base_resource.rb +14 -0
  6. data/lib/angus/command.rb +27 -0
  7. data/lib/angus/generator.rb +141 -0
  8. data/lib/angus/generator/templates/Gemfile +5 -0
  9. data/lib/angus/generator/templates/README.md +0 -0
  10. data/lib/angus/generator/templates/config.ru.erb +10 -0
  11. data/lib/angus/generator/templates/definitions/messages.yml +0 -0
  12. data/lib/angus/generator/templates/definitions/operations.yml.erb +21 -0
  13. data/lib/angus/generator/templates/definitions/representations.yml +0 -0
  14. data/lib/angus/generator/templates/definitions/service.yml.erb +3 -0
  15. data/lib/angus/generator/templates/resources/resource.rb.erb +6 -0
  16. data/lib/angus/generator/templates/services/service.rb.erb +4 -0
  17. data/lib/angus/marshallings/base.rb +2 -0
  18. data/lib/angus/marshallings/marshalling.rb +136 -0
  19. data/lib/angus/marshallings/unmarshalling.rb +29 -0
  20. data/lib/angus/renders/base.rb +2 -0
  21. data/lib/angus/renders/html_render.rb +9 -0
  22. data/lib/angus/renders/json_render.rb +15 -0
  23. data/lib/angus/request_handler.rb +62 -0
  24. data/lib/angus/resource_definition.rb +78 -0
  25. data/lib/angus/response.rb +53 -0
  26. data/lib/angus/responses.rb +232 -0
  27. data/lib/angus/rspec/spec_helper.rb +3 -0
  28. data/lib/angus/rspec/support/examples.rb +64 -0
  29. data/lib/angus/rspec/support/examples/describe_errors.rb +36 -0
  30. data/lib/angus/rspec/support/matchers/operation_response_matchers.rb +117 -0
  31. data/lib/angus/rspec/support/operation_response.rb +57 -0
  32. data/lib/angus/utils.rb +2 -0
  33. data/lib/angus/utils/params.rb +24 -0
  34. data/lib/angus/utils/string.rb +27 -0
  35. data/lib/angus/version.rb +2 -2
  36. data/spec/angus/rspec/examples/describe_errors_spec.rb +64 -0
  37. data/spec/spec_helper.rb +27 -0
  38. metadata +181 -19
  39. data/.gitignore +0 -17
  40. data/Gemfile +0 -4
  41. data/LICENSE.txt +0 -22
  42. data/README.md +0 -29
  43. data/Rakefile +0 -1
  44. data/angus.gemspec +0 -23
@@ -0,0 +1,136 @@
1
+ require 'bigdecimal'
2
+ require 'date'
3
+
4
+ module Angus
5
+ module Marshalling
6
+
7
+ # Marshal an object
8
+ #
9
+ # This method is intended for scalar objects and arrays of scalar objects.
10
+ #
11
+ # For more complex objects:
12
+ # @see Angus::Marshalling.marshal_object
13
+ #
14
+ # @param object The object to be marshalled
15
+ # @return [Array] An object suitable for be converted easily to JSON notation
16
+ #
17
+ # If {object} is an array, this method returns an array with all the elements marshalled
18
+ # Else returns a marshalled representation of the object.
19
+ def self.marshal(object)
20
+ if object.is_a?(Array)
21
+ object.map { |element| marshal(element) }
22
+ else
23
+ marshal_scalar(object)
24
+ end
25
+ end
26
+
27
+ # Marshal a complex object.
28
+ #
29
+ # @param object The object to be marshalled
30
+ # @param getters An array of getters / hashes that will be used to obtain the information
31
+ # from the object.
32
+ def self.marshal_object(object, getters)
33
+ result = {}
34
+ getters.each do |getter|
35
+ if getter.is_a?(Hash)
36
+ key = getter.keys[0]
37
+ value = get_value(object, key)
38
+
39
+ #TODO Consider adding ActiveRecord::Relation support
40
+ #ex: "if value.is_a?(Array) || value.is_a?(ActiveRecord::Relation)"
41
+ if value.is_a?(Array)
42
+ result[key] = value.map { |object| marshal_object(object, getter.values[0]) }
43
+ else
44
+ result[key] = value.nil? ? nil : marshal_object(value, getter.values[0])
45
+ end
46
+ else
47
+ value = get_value(object, getter)
48
+ result[getter] = marshal_scalar(value)
49
+ end
50
+ end
51
+ return result
52
+ end
53
+
54
+ private
55
+
56
+ # Gets a value from a object for a given getter.
57
+ #
58
+ # @param [Object] object The object to get the value from
59
+ # @param [Symbol, String] getter to request from the object
60
+ # @raise [InvalidGetterError] when getter is not present
61
+ # in the object
62
+ # @return [Object] the requested value
63
+ def self.get_value(object, getter)
64
+ if object.is_a?(Hash)
65
+ get_value_from_hash(object, getter)
66
+ else
67
+ get_value_from_object(object, getter)
68
+ end
69
+ end
70
+
71
+ # Gets a value from a object by invoking method.
72
+ #
73
+ # @param [Object] object The object to get the value from
74
+ # @param [Symbol, String] method the method to invoke in the object
75
+ # @raise [InvalidGetterError] when the object does not responds
76
+ # to method as public.
77
+ # @return [Object] the requested value
78
+ def self.get_value_from_object(object, method)
79
+ value = object.public_send method.to_sym
80
+
81
+ # HACK in order to fix the error:
82
+ # NoMethodError: undefined method `merge' for #<JSON::Ext::Generator::State:0x257fd086>
83
+ if value.is_a?(Array)
84
+ value.to_a
85
+ else
86
+ value
87
+ end
88
+ # TODO ver si tirar una exception especifica o que hacer?
89
+ #rescue NoMethodError => error
90
+ # raise InvalidGetterError.new(method, error.backtrace.first)
91
+ end
92
+
93
+ # Gets a value from a hash for a given key.
94
+ #
95
+ # It accepts the key as a symbol or a string,
96
+ # it looks for the symbol key first.
97
+ #
98
+ # @param [Hash] hash The hash to get the value from
99
+ # @param [Symbol, String] key to get from the hash
100
+ # @raise [InvalidGetterError] when the key does not
101
+ # exists in the hash
102
+ # @return [Object] the requested value
103
+ def self.get_value_from_hash(hash, key)
104
+ if hash.has_key?(key.to_sym)
105
+ hash[key.to_sym]
106
+ elsif hash.has_key?(key.to_s)
107
+ hash[key.to_s]
108
+ else
109
+ #raise InvalidGetterError.new(key)
110
+ raise NoMethodError.new(key.to_s)
111
+ end
112
+ end
113
+
114
+ # Marshal a scalar value
115
+ # @param scalar the scalar value to be marshalled
116
+ #
117
+ # If scalar is a Date or DateTime it return a iso8601 string
118
+ # If scalar is a Symbol it returns the symbol as string
119
+ # Everything else (string, integer, ...) returns the same object.
120
+ def self.marshal_scalar(scalar)
121
+ case scalar
122
+ when Time
123
+ scalar.iso8601
124
+ when DateTime
125
+ scalar.iso8601
126
+ when Date
127
+ scalar.iso8601
128
+ when Symbol
129
+ scalar.to_s
130
+ else
131
+ scalar
132
+ end
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,29 @@
1
+ require 'bigdecimal'
2
+ require 'date'
3
+
4
+ module Angus
5
+ module Unmarshalling
6
+
7
+ def self.unmarshal_scalar(scalar, type)
8
+ return nil if scalar.nil?
9
+
10
+ case type
11
+ when :string
12
+ scalar
13
+ when :integer
14
+ scalar
15
+ when :boolean
16
+ scalar
17
+ when :date
18
+ Date.iso8601(scalar)
19
+ when :date_time
20
+ DateTime.iso8601(scalar)
21
+ when :decimal
22
+ BigDecimal.new(scalar.to_s)
23
+ else
24
+ raise ArgumentError, "Unknown type: #{type}"
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'html_render'
2
+ require_relative 'json_render'
@@ -0,0 +1,9 @@
1
+ module HtmlRender
2
+
3
+ def self.render(response, html)
4
+ response['Content-Type'] = 'text/html'
5
+
6
+ response.write(html)
7
+ end
8
+
9
+ end
@@ -0,0 +1,15 @@
1
+ module JsonRender
2
+
3
+ def self.render(response, json)
4
+ response['Content-Type'] = 'application/json'
5
+
6
+ json = if json.is_a?(String)
7
+ json
8
+ else
9
+ JSON(json, :ascii_only => true)
10
+ end
11
+
12
+ response.write(json)
13
+ end
14
+
15
+ end
@@ -0,0 +1,62 @@
1
+ require 'angus-router'
2
+
3
+ require_relative 'response'
4
+ require_relative 'responses'
5
+
6
+ module Angus
7
+ class RequestHandler
8
+
9
+ include Responses
10
+
11
+ DEFAULT_RENDER = :json
12
+
13
+ attr_reader :env, :request, :response, :params
14
+
15
+ def initialize
16
+ @router = Angus::Router.new
17
+ end
18
+
19
+ def router
20
+ @router
21
+ end
22
+
23
+ def call(env)
24
+ begin
25
+ @env = env
26
+ @response = Response.new
27
+
28
+ router.route(env)
29
+ rescue Angus::Router::NotImplementedError
30
+ @response.status = HTTP_STATUS_CODE_NOT_FOUND
31
+
32
+ render({ 'status' => 'error',
33
+ 'messages' => [{ 'level' => 'error', 'key' => 'RouteNotFound',
34
+ 'dsc' => 'Invalid route' }]
35
+ }, {format: :json})
36
+ end
37
+
38
+ @response.finish
39
+ end
40
+
41
+ # TODO ver multiples formatos en el futuro
42
+ def render(content, options = {})
43
+ format = options[:format] || DEFAULT_RENDER
44
+ case(format)
45
+ when :html
46
+ HtmlRender.render(@response, content)
47
+ when :json
48
+ JsonRender.render(@response, content)
49
+ else
50
+ raise 'Unknown render format'
51
+ end
52
+ end
53
+
54
+ # TODO ver esto en el futuro
55
+ # Use the specified Rack middleware
56
+ def use(middleware, *args, &block)
57
+ @prototype = nil
58
+ @middleware << [middleware, args, block]
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ require 'yaml'
2
+
3
+ module Angus
4
+ class ResourceDefinition
5
+
6
+ def initialize(resource_name, representations)
7
+ @resource_name = resource_name
8
+ @resource_class_name = classify_resource(resource_name)
9
+ @representations = representations || {}
10
+ end
11
+
12
+ def operations
13
+ @representations.operations[@resource_name.to_s]
14
+ end
15
+
16
+ def canonical_name
17
+ Angus::String.underscore(@resource_class_name.to_s)
18
+ end
19
+
20
+ def resource_class
21
+ return @resource_class if @resource_class
22
+ require resource_path
23
+
24
+ @resource_class = Object.const_get(@resource_class_name)
25
+ end
26
+
27
+ def resource_path
28
+ File.join('resources', canonical_name)
29
+ end
30
+
31
+ def build_response_metadata(response_representation)
32
+ return {} unless response_representation
33
+
34
+ case response_representation
35
+ when Angus::SDoc::Definitions::Representation
36
+ result = []
37
+
38
+ response_representation.fields.each do |field|
39
+ result << build_response_metadata(field)
40
+ end
41
+
42
+ result
43
+ when Array
44
+ result = []
45
+
46
+ response_representation.each do |field|
47
+ result << build_response_metadata(field)
48
+ end
49
+
50
+ result
51
+ else
52
+ field_name = response_representation.name
53
+ field_type = response_representation.type || response_representation.elements_type
54
+
55
+ # TODO fix this
56
+ representation = representation_by_name(field_type)
57
+
58
+ if representation.nil?
59
+ field_name.to_sym
60
+ else
61
+ {field_name.to_sym => build_response_metadata(representation)}
62
+ end
63
+ end
64
+ end
65
+
66
+ # TODO improve this find
67
+ def representation_by_name(name)
68
+ @representations.representations.find { |representation| representation.name == name }
69
+ end
70
+
71
+ private
72
+
73
+ def classify_resource(resource)
74
+ Angus::String.camelize(resource)
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ module Angus
2
+ class Response < Rack::Response
3
+ def initialize(*)
4
+ super
5
+ headers['Content-Type'] ||= 'text/html'
6
+ end
7
+
8
+ def body=(value)
9
+ value = value.body while Rack::Response === value
10
+ @body = String === value ? [value.to_str] : value
11
+ end
12
+
13
+ def each
14
+ block_given? ? super : enum_for(:each)
15
+ end
16
+
17
+ def finish
18
+ result = body
19
+
20
+ if drop_content_info?
21
+ headers.delete "Content-Length"
22
+ headers.delete "Content-Type"
23
+ end
24
+
25
+ if drop_body?
26
+ close
27
+ result = []
28
+ end
29
+
30
+ if calculate_content_length?
31
+ # if some other code has already set Content-Length, don't muck with it
32
+ # currently, this would be the static file-handler
33
+ headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
34
+ end
35
+
36
+ [status.to_i, headers, result]
37
+ end
38
+
39
+ private
40
+
41
+ def calculate_content_length?
42
+ headers["Content-Type"] and not headers["Content-Length"] and Array === body
43
+ end
44
+
45
+ def drop_content_info?
46
+ status.to_i / 100 == 1 or drop_body?
47
+ end
48
+
49
+ def drop_body?
50
+ [204, 205, 304].include?(status.to_i)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,232 @@
1
+ require 'angus/sdoc'
2
+
3
+ module Angus
4
+ module Responses
5
+
6
+ HTTP_STATUS_CODE_OK = 200
7
+
8
+ HTTP_STATUS_CODE_FORBIDDEN = 403
9
+ HTTP_STATUS_CODE_NOT_FOUND = 404
10
+ HTTP_STATUS_CODE_CONFLICT = 409
11
+ HTTP_STATUS_CODE_UNPROCESSABLE_ENTITY = 422
12
+
13
+ HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR = 500
14
+
15
+ # Returns a suitable HTTP status code for the given error
16
+ #
17
+ # If error param responds to #errors, then #{HTTP_STATUS_CODE_CONFLICT} will be returned.
18
+ #
19
+ # If error param responds to #error_key, then the status_code associated
20
+ # with the message will be returned.
21
+ #
22
+ # @param [#errors, #error_key] error An error object
23
+ #
24
+ # @return [Integer] HTTP status code
25
+ def get_error_status_code(error)
26
+ if error.respond_to?(:errors)
27
+ return HTTP_STATUS_CODE_CONFLICT
28
+ end
29
+
30
+ message = get_error_definition(error)
31
+
32
+ if message
33
+ message.status_code
34
+ else
35
+ HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR
36
+ end
37
+ end
38
+
39
+ # Returns the error definition.
40
+ #
41
+ # If the error does not responds to error_key nil will be returned, see EvolutionError.
42
+ #
43
+ # @param [#error_key] error An error object
44
+ #
45
+ # @return [Hash]
46
+ def get_error_definition(error)
47
+ error_key = error.class.name
48
+
49
+ get_message_definition(error_key, Angus::SDoc::Definitions::Message::ERROR_LEVEL)
50
+ end
51
+
52
+ def get_message_definition(key, level)
53
+ message = @definitions.messages.find { |name, definition|
54
+ name == key.to_s && definition.level.downcase == level.downcase
55
+ }
56
+
57
+ message.last if message
58
+ end
59
+
60
+ # Builds a service success response
61
+ #
62
+ # @param [Hash<Symbol, Object>] messages Elements to be sent in the response
63
+ # @param [Array<ResponseMessage] messages Messages to be sent in the response
64
+ #
65
+ # @return [String] JSON response
66
+ def build_success_response(elements = {}, messages = [])
67
+ elements = {
68
+ :status => :success,
69
+ }.merge(elements)
70
+
71
+ unless messages.empty?
72
+ elements[:messages] = messages
73
+ end
74
+
75
+ json(elements)
76
+ end
77
+
78
+ # Builds a service error response
79
+ def build_error_response(error)
80
+ error_messages = messages_from_error(error)
81
+ build_response(:error, *error_messages)
82
+ end
83
+
84
+ # Builds a ResponseMessage object
85
+ #
86
+ # @param [#to_s] key Message key
87
+ # @param [#to_s] level Message level
88
+ # @param [*Object] params Objects to be used when formatting the message description
89
+ #
90
+ # @raise [NameError] when there's no message for the given key and level
91
+ #
92
+ # @return [ResponseMessage]
93
+ def build_message(key, level, *params)
94
+ message_definition = get_message_definition(key, level)
95
+
96
+ unless message_definition
97
+ raise NameError.new("Could not found message with key: #{key}, level: #{level}")
98
+ end
99
+
100
+ description = if message_definition.text
101
+ message_definition.text % params
102
+ else
103
+ message_definition.description
104
+ end
105
+
106
+ Angus::SDoc::Definitions::Message
107
+
108
+ message = Angus::SDoc::Definitions::Message.new
109
+ message.key = key
110
+ message.level = level
111
+ message.description = description
112
+
113
+ message
114
+ end
115
+
116
+ # Builds a service success response
117
+ def build_warning_response(error)
118
+ error_messages = messages_from_error(error, :warning)
119
+ build_response(:success, *error_messages)
120
+ end
121
+
122
+ # Builds a success response with the received data
123
+ #
124
+ # @param [Hash] data the hash to be returned in the response
125
+ # @param [Array] attributes the attributes that will be returned
126
+ # @param [Message, Symbol, String] messages A list of messages, or message keys
127
+ def build_data_response(data, attributes, messages = [])
128
+ marshalled_data = Angus::Marshalling.marshal_object(data, attributes)
129
+
130
+ messages = build_messages(Angus::SDoc::Definitions::Message::INFO_LEVEL, messages)
131
+
132
+ build_success_response(marshalled_data, messages)
133
+ end
134
+
135
+ # Builds a success response with no elements
136
+ #
137
+ # The response would include the following:
138
+ # - status
139
+ # - messages
140
+ #
141
+ # @param [Array<ResponseMessage>, Array<Symbol>] messages Message to be included in the response
142
+ #
143
+ # @return [String] JSON response
144
+ def build_no_data_response(messages = [])
145
+ messages = build_messages(Angus::SDoc::Definitions::Message::INFO_LEVEL, messages)
146
+
147
+ build_success_response({}, messages)
148
+ end
149
+
150
+ # Builds a list of messages with the following level
151
+ #
152
+ # ResponseMessage objects contained in messages param won't be modified, this method
153
+ # only creates ResponseMessage for each Symbol in messages array
154
+ #
155
+ # @param [#to_s] level Messages level
156
+ # @param [Array<ResponseMessage>, Array<Symbol>] messages
157
+ #
158
+ # @raise (see #build_message)
159
+ #
160
+ # @return [Array<ResponseMessage>]
161
+ def build_messages(level, messages)
162
+ (messages || []).map do |message|
163
+ if message.kind_of?(Angus::SDoc::Definitions::Message)
164
+ message
165
+ else
166
+ build_message(message, level)
167
+ end
168
+ end
169
+ end
170
+
171
+ # Sets the content_type to json and serialize +element+ as json
172
+ def json(element)
173
+ #content_type :json
174
+ JSON(element, :ascii_only => true)
175
+ end
176
+
177
+ private
178
+ # Returns an array of messages errors to be sent in an operation response
179
+ #
180
+ # If {error} respond_to? :errors then the method returns one error message
181
+ # for each one.
182
+ #
183
+ # Each message returned is a hash with:
184
+ # - level
185
+ # - key
186
+ # - description
187
+ #
188
+ # @param [Exception] error The error to be returned
189
+ #
190
+ # @return [Array] an array of messages
191
+ def messages_from_error(error, level = :error)
192
+ messages = []
193
+
194
+ if error.respond_to?(:errors)
195
+ error.errors.each do |key, description|
196
+ messages << {:level => level, :key => key, :dsc => description}
197
+ end
198
+ elsif error.respond_to?(:error_key)
199
+ messages << {:level => level, :key => error.error_key,
200
+ :dsc => error_message(error)}
201
+ else
202
+ messages << {:level => level, :key => error.class.name, :dsc => error.message}
203
+ end
204
+
205
+ messages
206
+ end
207
+
208
+ # Returns the message for an error.
209
+ #
210
+ # It first tries to get the message from text attribute of the error definition
211
+ # if no definition is found or if the text attribute is blank it the returns the error
212
+ # message attribute.
213
+ #
214
+ # @param [Exception] error The error to get the message for.
215
+ #
216
+ # @return [String] the error message.
217
+ def error_message(error)
218
+ error_definition = get_error_definition(error)
219
+
220
+ if error_definition && !error_definition.text.blank?
221
+ error_definition.text
222
+ else
223
+ error.message
224
+ end
225
+ end
226
+
227
+ def build_response(status, *messages)
228
+ json(:status => status, :messages => messages)
229
+ end
230
+
231
+ end
232
+ end