extended_her 0.5

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