angus 0.0.1 → 0.0.2

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