extended_her 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CONTRIBUTING.md +26 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +7 -0
  7. data/README.md +723 -0
  8. data/Rakefile +11 -0
  9. data/UPGRADE.md +32 -0
  10. data/examples/twitter-oauth/Gemfile +13 -0
  11. data/examples/twitter-oauth/app.rb +50 -0
  12. data/examples/twitter-oauth/config.ru +5 -0
  13. data/examples/twitter-oauth/views/index.haml +9 -0
  14. data/examples/twitter-search/Gemfile +12 -0
  15. data/examples/twitter-search/app.rb +55 -0
  16. data/examples/twitter-search/config.ru +5 -0
  17. data/examples/twitter-search/views/index.haml +9 -0
  18. data/extended_her.gemspec +27 -0
  19. data/lib/her.rb +23 -0
  20. data/lib/her/api.rb +108 -0
  21. data/lib/her/base.rb +17 -0
  22. data/lib/her/collection.rb +12 -0
  23. data/lib/her/errors.rb +5 -0
  24. data/lib/her/exceptions/exception.rb +4 -0
  25. data/lib/her/exceptions/record_invalid.rb +8 -0
  26. data/lib/her/exceptions/record_not_found.rb +13 -0
  27. data/lib/her/middleware.rb +9 -0
  28. data/lib/her/middleware/accept_json.rb +15 -0
  29. data/lib/her/middleware/first_level_parse_json.rb +34 -0
  30. data/lib/her/middleware/second_level_parse_json.rb +28 -0
  31. data/lib/her/model.rb +69 -0
  32. data/lib/her/model/base.rb +7 -0
  33. data/lib/her/model/hooks.rb +114 -0
  34. data/lib/her/model/http.rb +284 -0
  35. data/lib/her/model/introspection.rb +57 -0
  36. data/lib/her/model/orm.rb +191 -0
  37. data/lib/her/model/orm/comparison_methods.rb +20 -0
  38. data/lib/her/model/orm/create_methods.rb +29 -0
  39. data/lib/her/model/orm/destroy_methods.rb +53 -0
  40. data/lib/her/model/orm/error_methods.rb +19 -0
  41. data/lib/her/model/orm/fields_definition.rb +15 -0
  42. data/lib/her/model/orm/find_methods.rb +46 -0
  43. data/lib/her/model/orm/persistance_methods.rb +22 -0
  44. data/lib/her/model/orm/relation_mapper.rb +21 -0
  45. data/lib/her/model/orm/save_methods.rb +58 -0
  46. data/lib/her/model/orm/serialization_methods.rb +28 -0
  47. data/lib/her/model/orm/update_methods.rb +31 -0
  48. data/lib/her/model/paths.rb +82 -0
  49. data/lib/her/model/relationships.rb +191 -0
  50. data/lib/her/paginated_collection.rb +20 -0
  51. data/lib/her/relation.rb +94 -0
  52. data/lib/her/version.rb +3 -0
  53. data/spec/api_spec.rb +131 -0
  54. data/spec/collection_spec.rb +26 -0
  55. data/spec/middleware/accept_json_spec.rb +10 -0
  56. data/spec/middleware/first_level_parse_json_spec.rb +42 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +25 -0
  58. data/spec/model/hooks_spec.rb +406 -0
  59. data/spec/model/http_spec.rb +184 -0
  60. data/spec/model/introspection_spec.rb +59 -0
  61. data/spec/model/orm_spec.rb +552 -0
  62. data/spec/model/paths_spec.rb +286 -0
  63. data/spec/model/relationships_spec.rb +222 -0
  64. data/spec/model_spec.rb +31 -0
  65. data/spec/spec_helper.rb +46 -0
  66. metadata +222 -0
@@ -0,0 +1,57 @@
1
+ module Her
2
+ module Model
3
+ module Introspection
4
+ extend ActiveSupport::Concern
5
+ # Inspect an element, returns it for introspection.
6
+ #
7
+ # @example
8
+ # class User
9
+ # include Her::Model
10
+ # end
11
+ #
12
+ # @user = User.find(1)
13
+ # p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
14
+ def inspect
15
+ "#<#{self.class} #{@data.keys.map { |k| "#{k}: #{attribute_for_inspect(send(k))}" }.join(", ")}>"
16
+ end
17
+
18
+ private
19
+ # @private
20
+ def attribute_for_inspect(value)
21
+ if value.is_a?(String) && value.length > 50
22
+ "#{value[0..50]}...".inspect
23
+ elsif value.is_a?(Date) || value.is_a?(Time)
24
+ %("#{value}")
25
+ else
26
+ value.inspect
27
+ end
28
+ end
29
+
30
+ module ClassMethods
31
+ # Finds a class at the same level as this one or at the global level.
32
+ # @private
33
+ def nearby_class(name)
34
+ sibling_class(name) || name.constantize rescue nil
35
+ end
36
+
37
+ protected
38
+ # Looks for a class at the same level as this one with the given name.
39
+ # @private
40
+ def sibling_class(name)
41
+ if mod = self.containing_module
42
+ @sibling_class ||= {}
43
+ @sibling_class[mod] ||= {}
44
+ @sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
45
+ end
46
+ end
47
+
48
+ # If available, returns the containing Module for this class.
49
+ # @private
50
+ def containing_module
51
+ return unless self.name =~ /::/
52
+ self.name.split("::")[0..-2].join("::").constantize
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,191 @@
1
+ module Her
2
+ module Model
3
+ # This module adds ORM-like capabilities to the model
4
+ module ORM
5
+ extend ActiveSupport::Concern
6
+ include CreateMethods
7
+ include DestroyMethods
8
+ include FindMethods
9
+ include SaveMethods
10
+ include UpdateMethods
11
+ include RelationMapper
12
+ include ErrorMethods
13
+ include ComparisonMethods
14
+ include PersistanceMethods
15
+ include SerializationMethods
16
+ include FieldsDefinition
17
+
18
+ attr_accessor :data, :metadata, :errors
19
+ alias :attributes :data
20
+ alias :attributes= :data=
21
+
22
+ # Initialize a new object with data received from an HTTP request
23
+ def initialize(params={})
24
+ fields = self.class.instance_variable_defined?(:@fields) ? self.class.instance_variable_get(:@fields) : []
25
+ @data = Hash[fields.map{ |field| [field, nil] }]
26
+ @metadata = params.delete(:_metadata) || {}
27
+ @errors = params.delete(:_errors) || {}
28
+
29
+ # Use setter methods first, then translate attributes of relationships
30
+ # into relationship instances, then merge the parsed_data into @data.
31
+ unset_data = Her::Model::ORM.use_setter_methods(self, params)
32
+ parsed_data = self.class.parse_relationships(unset_data)
33
+ @data.update(convert_types(parsed_data))
34
+ end
35
+
36
+ def convert_types(parsed_data)
37
+ parsed_data.each do |key, value|
38
+ "2013-01-26T09:29:39.358Z"
39
+ if value.is_a?(String)
40
+ m = value.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z/)
41
+ next if m.nil?
42
+ if m[1].nil?
43
+ parsed_data[key] = Time.strptime(value, '%Y-%m-%dT%H:%M:%S%Z')
44
+ else
45
+ parsed_data[key] = Time.strptime(value, '%Y-%m-%dT%H:%M:%S.%L%Z')
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # Initialize a collection of resources
52
+ # @private
53
+ def self.initialize_collection(klass, parsed_data={})
54
+ collection_data = parsed_data[:data].map do |item_data|
55
+ resource = klass.new(klass.parse(item_data))
56
+ klass.wrap_in_hooks(resource, :find)
57
+ resource
58
+ end
59
+
60
+ collection_class = Her::Collection
61
+ if parsed_data[:metadata].is_a?(Hash) && parsed_data[:metadata].has_key?(:current_page)
62
+ collection_class = Her::PaginatedCollection
63
+ end
64
+ collection_class.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
65
+ end
66
+
67
+ # Handles missing methods by routing them through @data
68
+ # @private
69
+ def method_missing(method, *args, &blk)
70
+ if method.to_s.end_with?('=')
71
+ @data[method.to_s.chomp('=').to_sym] = args.first
72
+ elsif method.to_s.end_with?('?')
73
+ @data.include?(method.to_s.chomp('?').to_sym)
74
+ elsif @data.include?(method)
75
+ @data[method]
76
+ else
77
+ super
78
+ end
79
+ end
80
+
81
+ # Handles returning true for the cases handled by method_missing
82
+ def respond_to?(method, include_private = false)
83
+ method.to_s.end_with?('=') || method.to_s.end_with?('?') || @data.include?(method) || super
84
+ end
85
+
86
+ # Assign new data to an instance
87
+ def assign_data(new_data)
88
+ new_data = Her::Model::ORM.use_setter_methods(self, new_data)
89
+ @data.update new_data
90
+ end
91
+ alias :assign_attributes :assign_data
92
+
93
+ # Handles returning true for the accessible attributes
94
+ def has_data?(attribute_name)
95
+ @data.include?(attribute_name)
96
+ end
97
+
98
+ # Handles returning attribute value from data
99
+ def get_data(attribute_name)
100
+ @data[attribute_name]
101
+ end
102
+
103
+ # Override the method to prevent from returning the object ID (in ruby-1.8.7)
104
+ # @private
105
+ def id
106
+ @data[:id] || super
107
+ end
108
+
109
+ # Use setter methods of model for each key / value pair in params
110
+ # Return key / value pairs for which no setter method was defined on the model
111
+ # @private
112
+ def self.use_setter_methods(model, params)
113
+ setter_method_names = model.class.setter_method_names
114
+ params.inject({}) do |memo, (key, value)|
115
+ setter_method = key.to_s + '='
116
+ if setter_method_names.include?(setter_method)
117
+ model.send(setter_method, value)
118
+ else
119
+ if key.is_a?(String)
120
+ key = key.to_sym
121
+ end
122
+ memo[key] = value
123
+ end
124
+ memo
125
+ end
126
+ end
127
+
128
+ module ClassMethods
129
+ # Initialize a collection of resources with raw data from an HTTP request
130
+ #
131
+ # @param [Array] parsed_data
132
+ def new_collection(parsed_data)
133
+ Her::Model::ORM.initialize_collection(self, parsed_data)
134
+ end
135
+
136
+ # Parse data before assigning it to a resource
137
+ #
138
+ # @param [Hash] data
139
+ def parse(data)
140
+ if parse_root_in_json
141
+ parse_root_in_json == true ? data[root_element.to_sym] : data[parse_root_in_json]
142
+ else
143
+ data
144
+ end
145
+ end
146
+
147
+ # Save an existing resource and return it
148
+ #
149
+ # @example
150
+ # @user = User.save_existing(1, { fullname: "Tobias Fünke" })
151
+ # # Called via PUT "/users/1"
152
+ def save_existing(id, params)
153
+ resource = new(params.merge(id: id))
154
+ resource.save
155
+ resource
156
+ end
157
+
158
+ # Destroy an existing resource
159
+ #
160
+ # @example
161
+ # User.destroy_existing(1)
162
+ # # Called via DELETE "/users/1"
163
+ def destroy_existing(id, params={})
164
+ request(params.merge(_method: :delete, _path: build_request_path(params.merge(id: id)))) do |parsed_data|
165
+ new(parse(parsed_data[:data]))
166
+ end
167
+ end
168
+
169
+ # @private
170
+ def setter_method_names
171
+ @setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
172
+ memo << method_name.to_s if method_name.to_s.end_with?('=')
173
+ memo
174
+ end
175
+ end
176
+
177
+ # Return or change the value of `include_root_in_json`
178
+ def include_root_in_json(value=nil)
179
+ return @include_root_in_json if value.nil?
180
+ @include_root_in_json = value
181
+ end
182
+
183
+ # Return or change the value of `parse_root_in`
184
+ def parse_root_in_json(value=nil)
185
+ return @parse_root_in_json if value.nil?
186
+ @parse_root_in_json = value
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,20 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module ComparisonMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Return `true` if the other object is also a Her::Model and has matching data
8
+ def ==(other)
9
+ other.is_a?(Her::Model) && @data == other.data
10
+ end
11
+
12
+ # Delegate to the == method
13
+ def eql?(other)
14
+ self == other
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,29 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module CreateMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Create a resource and return it
9
+ #
10
+ # @example
11
+ # @user = User.create({ fullname: "Tobias Fünke" })
12
+ # # Called via POST "/users/1"
13
+ def create(params = {})
14
+ resource = new(params)
15
+ resource.save
16
+ resource
17
+ end
18
+
19
+ def create!(params = {})
20
+ resource = create(params)
21
+ raise RecordInvalid.new(resource.errors) unless resource.errors.empty?
22
+
23
+ resource
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module DestroyMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Destroy a resource
8
+ #
9
+ # @example
10
+ # @user = User.find(1)
11
+ # @user.destroy
12
+ # # Called via DELETE "/users/1"
13
+ def destroy
14
+ resource = self
15
+ self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
16
+ klass.request(_method: :delete, _path: request_path) do |parsed_data|
17
+ self.data = self.class.parse(parsed_data[:data])
18
+ self.metadata = parsed_data[:metadata]
19
+ self.errors = parsed_data[:errors]
20
+ end
21
+ end
22
+ self
23
+ end
24
+
25
+ def delete
26
+ resource = self
27
+ self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
28
+ klass.request({_method: :delete, _path: build_request_path(params.merge(soft: true))}) do |parsed_data|
29
+ self.data = self.class.parse(parsed_data[:data])
30
+ self.metadata = parsed_data[:metadata]
31
+ self.errors = parsed_data[:errors]
32
+ end
33
+ end
34
+ self
35
+ end
36
+
37
+ module ClassMethods
38
+ def destroy(ids)
39
+ is_array = ids.is_a?(Array)
40
+ ids = Array(ids)
41
+ collection = ids.map{ |id| new(id: id).destroy }
42
+ collection = collection.first unless is_array
43
+ collection
44
+ end
45
+
46
+ def delete(ids)
47
+ Array(ids).map{ |id| new(id: id).delete }.count
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module ErrorMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Return `true` if a resource does not contain errors
8
+ def valid?
9
+ errors.empty?
10
+ end
11
+
12
+ # Return `true` if a resource contains errors
13
+ def invalid?
14
+ errors.any?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module FieldsDefinition
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def fields(*fields)
9
+ self.instance_variable_set(:@fields, fields)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module FindMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Fetch specific resource(s) by their ID
9
+ #
10
+ # @example
11
+ # @user = User.find(1)
12
+ # # Fetched via GET "/users/1"
13
+ #
14
+ # @example
15
+ # @users = User.find([1, 2])
16
+ # # Fetched via GET "/users/1" and GET "/users/2"
17
+ def find(*ids)
18
+ params = ids.last.is_a?(Hash) ? ids.pop : {}
19
+ prepared_ids = ids.flatten.compact.uniq
20
+ results = prepared_ids.map do |id|
21
+ resource = nil
22
+ request(params.merge(_method: :get, _path: build_request_path(params.merge(id: id)))) do |parsed_data|
23
+ resource = new(parse(parsed_data[:data]).merge _metadata: parsed_data[:data], _errors: parsed_data[:errors])
24
+ wrap_in_hooks(resource, :find)
25
+ end
26
+ resource
27
+ end
28
+ if ids.length > 1 || ids.first.kind_of?(Array)
29
+ raise RecordNotFound.one(self.class, ids) if results.nil?
30
+ results
31
+ else
32
+ raise RecordNotFound.some(self.class, ids, results.length, prepared_ids.length) if results.length != prepared_ids.length
33
+ results.first
34
+ end
35
+ end
36
+
37
+ def find_by_id(id)
38
+ find(id)
39
+ rescue RecordNotFound
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module PersistanceMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ def new?
8
+ !@data.include?(:id)
9
+ end
10
+ alias_method :new_record?, :new?
11
+
12
+ def persisted?
13
+ !new_record?
14
+ end
15
+
16
+ def destroyed?
17
+ @metadata.include?(:destroyed) && @metadata[:destroyed] == true
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end