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,21 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module RelationMapper
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ %w[ where group having order limit offset all count first last paginate ].each do |name|
9
+ define_method(name) do |*args|
10
+ to_relation.send(name, *args)
11
+ end
12
+ end
13
+
14
+ def to_relation
15
+ ::Her::Relation.new(self)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module SaveMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Save a resource
8
+ #
9
+ # @example Save a resource after fetching it
10
+ # @user = User.find(1)
11
+ # # Fetched via GET "/users/1"
12
+ # @user.fullname = "Tobias Fünke"
13
+ # @user.save
14
+ # # Called via PUT "/users/1"
15
+ #
16
+ # @example Save a new resource by creating it
17
+ # @user = User.new({ fullname: "Tobias Fünke" })
18
+ # @user.save
19
+ # # Called via POST "/users"
20
+ def save
21
+ create_or_update
22
+ end
23
+
24
+ def save!
25
+ save
26
+ raise RecordInvalid.new(self.errors) unless self.errors.empty?
27
+
28
+ self
29
+ end
30
+
31
+ def create_or_update
32
+ params = to_params
33
+ resource = self
34
+
35
+ if @data[:id]
36
+ hooks = [:update, :save]
37
+ method = :put
38
+ else
39
+ hooks = [:create, :save]
40
+ method = :post
41
+ end
42
+
43
+ self.class.wrap_in_hooks(resource, *hooks) do |resource, klass|
44
+ klass.request(params.merge(_method: method, _path: request_path)) do |parsed_data|
45
+ self.data = self.class.parse(parsed_data[:data]) if parsed_data[:data].any?
46
+ self.metadata = parsed_data[:metadata]
47
+ self.errors = parsed_data[:errors]
48
+
49
+ return false if self.errors.any?
50
+ end
51
+ end
52
+
53
+ self
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module SerializationMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Delegate to @data, allowing models to act correctly in code like:
8
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
9
+ def hash
10
+ @data.hash
11
+ end
12
+
13
+ # Convert into a hash of request parameters
14
+ #
15
+ # @example
16
+ # @user.to_params
17
+ # # => { id: 1, name: 'John Smith' }
18
+ def to_params
19
+ if self.class.include_root_in_json
20
+ { (self.class.include_root_in_json == true ? self.class.root_element : self.class.include_root_in_json) => @data.dup }
21
+ else
22
+ @data.dup
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module Her
2
+ module Model
3
+ module ORM
4
+ module UpdateMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ # Update resource attribute
8
+ #
9
+ # @example Update resource attribute
10
+ # @user = User.find(1)
11
+ # # Fetched via GET "/users/1"
12
+ # @user.update_attributes(:fullname, "Tobias Fünke")
13
+ # # Called via PUT "/users/1"
14
+ def update_attribute(attribute, value)
15
+ send(attribute.to_s + '=', value)
16
+ save
17
+ end
18
+
19
+ def update_attributes(attributes)
20
+ self.data.merge!(attributes)
21
+ save
22
+ end
23
+
24
+ def update_attributes!(attributes)
25
+ self.data.merge!(attributes)
26
+ save!
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,82 @@
1
+ module Her
2
+ module Model
3
+ module Paths
4
+ extend ActiveSupport::Concern
5
+ # Return a path based on the collection path and a resource data
6
+ #
7
+ # @example
8
+ # class User
9
+ # include Her::Model
10
+ # collection_path "/utilisateurs"
11
+ # end
12
+ #
13
+ # User.find(1) # Fetched via GET /utilisateurs/1
14
+ def request_path
15
+ self.class.build_request_path(@data.dup)
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # Defines a custom collection path for the resource
21
+ #
22
+ # @example
23
+ # class User
24
+ # include Her::Model
25
+ # collection_path "/users"
26
+ # end
27
+ def collection_path(path=nil)
28
+ @her_collection_path ||= begin
29
+ superclass.collection_path.dup if superclass.respond_to?(:collection_path)
30
+ end
31
+
32
+ return @her_collection_path unless path
33
+ @her_resource_path = "#{path}/:id"
34
+ @her_collection_path = path
35
+ end
36
+
37
+ # Defines a custom resource path for the resource
38
+ #
39
+ # @example
40
+ # class User
41
+ # include Her::Model
42
+ # resource_path "/users/:id"
43
+ # end
44
+ def resource_path(path=nil)
45
+ @her_resource_path ||= begin
46
+ superclass.resource_path.dup if superclass.respond_to?(:resource_path)
47
+ end
48
+
49
+ return @her_resource_path unless path
50
+ @her_resource_path = path
51
+ end
52
+
53
+ # Return a custom path based on the collection path and variable parameters
54
+ #
55
+ # @example
56
+ # class User
57
+ # include Her::Model
58
+ # collection_path "/utilisateurs"
59
+ # end
60
+ #
61
+ # User.all # Fetched via GET /utilisateurs
62
+ def build_request_path(path=nil, parameters={})
63
+ unless path.is_a?(String)
64
+ parameters = path || {}
65
+ path = parameters.include?(:id) && !parameters[:id].nil? ? resource_path : collection_path
66
+ end
67
+
68
+ path.gsub(/:([\w_]+)/) do
69
+ # Look for :key or :_key, otherwise raise an exception
70
+ parameters.delete($1.to_sym) || parameters.delete("_#{$1}".to_sym) || raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path (#{path})."))
71
+ end
72
+ end
73
+
74
+ # Return or change the value of `root_element`
75
+ def root_element(value=nil)
76
+ return @root_element if value.nil?
77
+ @root_element = value
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,191 @@
1
+ module Her
2
+ module Model
3
+ # This module adds relationships to models
4
+ module Relationships
5
+ extend ActiveSupport::Concern
6
+
7
+ # Returns true if the model has a relationship_name relationship, false otherwise.
8
+ def has_relationship?(relationship_name)
9
+ relationships = self.class.relationships.values.flatten.map { |r| r[:name] }
10
+ relationships.include?(relationship_name)
11
+ end
12
+
13
+ # Returns the resource/collection corresponding to the relationship_name relationship.
14
+ def get_relationship(relationship_name)
15
+ send(relationship_name) if has_relationship?(relationship_name)
16
+ end
17
+
18
+ module ClassMethods
19
+ # Return @her_relationships, lazily initialized with copy of the
20
+ # superclass' her_relationships, or an empty hash.
21
+ #
22
+ # @private
23
+ def relationships
24
+ @her_relationships ||= begin
25
+ if superclass.respond_to?(:relationships)
26
+ superclass.relationships.dup
27
+ else
28
+ {}
29
+ end
30
+ end
31
+ end
32
+
33
+ # Parse relationships data after initializing a new object
34
+ #
35
+ # @private
36
+ def parse_relationships(data)
37
+ relationships.each_pair do |type, definitions|
38
+ definitions.each do |relationship|
39
+ name = relationship[:name]
40
+ next unless data[name]
41
+ klass = self.nearby_class(relationship[:class_name])
42
+ data[name] = case type
43
+ when :has_many
44
+ Her::Model::ORM.initialize_collection(klass, :data => data[name])
45
+ when :has_one, :belongs_to
46
+ klass.new(klass.parse(data[name]))
47
+ else
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ data
53
+ end
54
+
55
+ # Define an *has_many* relationship.
56
+ #
57
+ # @param [Symbol] name The name of the model
58
+ # @param [Hash] attrs Options (currently not used)
59
+ #
60
+ # @example
61
+ # class User
62
+ # include Her::API
63
+ # has_many :articles
64
+ # end
65
+ #
66
+ # class Article
67
+ # include Her::API
68
+ # end
69
+ #
70
+ # @user = User.find(1)
71
+ # @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
72
+ # # Fetched via GET "/users/1/articles"
73
+ def has_many(name, attrs={})
74
+ attrs = {
75
+ :class_name => name.to_s.classify,
76
+ :name => name,
77
+ :path => "/#{name}",
78
+ :inverse_of => nil
79
+ }.merge(attrs)
80
+ (relationships[:has_many] ||= []) << attrs
81
+
82
+ define_method(name) do |*method_attrs|
83
+ method_attrs = method_attrs[0] || {}
84
+ klass = self.class.nearby_class(attrs[:class_name])
85
+ if method_attrs.any?
86
+ @data[name] = klass.get_collection("#{self.class.build_request_path(method_attrs.merge(:id => id))}#{attrs[:path]}")
87
+ else
88
+ @data[name] ||= klass.get_collection("#{self.class.build_request_path(:id => id)}#{attrs[:path]}")
89
+ end
90
+
91
+ inverse_of = if attrs[:inverse_of]
92
+ attrs[:inverse_of]
93
+ else
94
+ self.class.name.split('::').last.tableize.singularize
95
+ end
96
+ @data[name].each do |entry|
97
+ entry.send("#{inverse_of}=", self)
98
+ end
99
+
100
+ @data[name]
101
+ end
102
+ end
103
+
104
+ # Define an *has_one* relationship.
105
+ #
106
+ # @param [Symbol] name The name of the model
107
+ # @param [Hash] attrs Options (currently not used)
108
+ #
109
+ # @example
110
+ # class User
111
+ # include Her::API
112
+ # has_one :organization
113
+ # end
114
+ #
115
+ # class Organization
116
+ # include Her::API
117
+ # end
118
+ #
119
+ # @user = User.find(1)
120
+ # @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
121
+ # # Fetched via GET "/users/1/organization"
122
+ def has_one(name, attrs={})
123
+ attrs = {
124
+ :class_name => name.to_s.classify,
125
+ :name => name,
126
+ :path => "/#{name}"
127
+ }.merge(attrs)
128
+ (relationships[:has_one] ||= []) << attrs
129
+
130
+ define_method(name) do |*method_attrs|
131
+ method_attrs = method_attrs[0] || {}
132
+ klass = self.class.nearby_class(attrs[:class_name])
133
+ if method_attrs.any?
134
+ klass.get_resource("#{self.class.build_request_path(method_attrs.merge(:id => id))}#{attrs[:path]}")
135
+ else
136
+ @data[name] ||= klass.get_resource("#{self.class.build_request_path(:id => id)}#{attrs[:path]}")
137
+ end
138
+ end
139
+ end
140
+
141
+ # Define a *belongs_to* relationship.
142
+ #
143
+ # @param [Symbol] name The name of the model
144
+ # @param [Hash] attrs Options (currently not used)
145
+ #
146
+ # @example
147
+ # class User
148
+ # include Her::API
149
+ # belongs_to :team, :class_name => "Group"
150
+ # end
151
+ #
152
+ # class Group
153
+ # include Her::API
154
+ # end
155
+ #
156
+ # @user = User.find(1)
157
+ # @user.team # => #<Team(teams/2) id=2 name="Developers">
158
+ # # Fetched via GET "/teams/2"
159
+ def belongs_to(name, attrs={})
160
+ attrs = {
161
+ :class_name => name.to_s.classify,
162
+ :name => name,
163
+ :foreign_key => "#{name}_id",
164
+ :path => "/#{name.to_s.pluralize}/:id"
165
+ }.merge(attrs)
166
+ (relationships[:belongs_to] ||= []) << attrs
167
+
168
+ define_method(name) do |*method_attrs|
169
+ method_attrs = method_attrs[0] || {}
170
+ klass = self.class.nearby_class(attrs[:class_name])
171
+ if method_attrs.any?
172
+ klass.get_resource("#{klass.build_request_path(method_attrs.merge(:id => @data[attrs[:foreign_key].to_sym]))}")
173
+ else
174
+ @data[name] ||= klass.get_resource("#{klass.build_request_path(:id => @data[attrs[:foreign_key].to_sym])}")
175
+ end
176
+ end
177
+ end
178
+
179
+ # @private
180
+ def relationship_accessor(type, attrs)
181
+ name = attrs[:name]
182
+ class_name = attrs[:class_name]
183
+ define_method(name) do
184
+ klass = self.class.nearby_class(attrs[:class_name])
185
+ @data[name] ||= klass.get_resource("#{klass.build_request_path(attrs[:path], :id => @data[attrs[:foreign_key].to_sym])}")
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,20 @@
1
+ module Her
2
+ class PaginatedCollection < Collection
3
+ def current_page
4
+ metadata[:current_page].to_i
5
+ end
6
+
7
+ def per_page
8
+ metadata[:per_page].to_i
9
+ end
10
+
11
+ def total_entries
12
+ metadata[:total_entries].to_i
13
+ end
14
+
15
+ def total_pages
16
+ return 0 if total_entries == 0 || per_page == 0
17
+ (total_entries.to_f / per_page).ceil
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,94 @@
1
+ module Her
2
+ class Relation
3
+ def initialize(model)
4
+ @model = model
5
+ @conditions = []
6
+ @group_conditions = []
7
+ @having_conditions = []
8
+ @order_conditions = []
9
+ @limit_value = nil
10
+ @offset_value = nil
11
+ @do_paginate = false
12
+ @do_count = false
13
+ end
14
+
15
+ def where(*args)
16
+ args = args.first if args.first.is_a?(Hash)
17
+ @conditions.push(args)
18
+ self
19
+ end
20
+
21
+ def group(*args)
22
+ @group_conditions += args
23
+ self
24
+ end
25
+
26
+ def having(*args)
27
+ @having_conditions += args
28
+ self
29
+ end
30
+
31
+ def order(*args)
32
+ @order_conditions += args
33
+ self
34
+ end
35
+
36
+ def limit(value)
37
+ @limit_value = value
38
+ self
39
+ end
40
+
41
+ def offset(value)
42
+ @offset_value = value
43
+ self
44
+ end
45
+
46
+ def all(params = {})
47
+ with_response(params) do |data|
48
+ @model.new_collection(data)
49
+ end
50
+ end
51
+
52
+ def count
53
+ @do_count = true
54
+ with_response do |data|
55
+ data.first
56
+ end
57
+ end
58
+
59
+ def first
60
+ order('id asc').limit(1)
61
+ all.first if all.respond_to?(:first)
62
+ end
63
+
64
+ def last
65
+ order('id desc').limit(1)
66
+ all.first if all.respond_to?(:first)
67
+ end
68
+
69
+ def paginate(page = 1, per_page = 20)
70
+ page = page.to_i < 1 ? 1 : page.to_i
71
+ per_page = per_page.to_i < 1 ? 20 : per_page.to_i
72
+
73
+ @do_paginate = true
74
+ offset((page - 1) * per_page).limit(per_page)
75
+ end
76
+
77
+ private
78
+
79
+ def with_response(params = {})
80
+ params[:her_special_where] = MultiJson.dump(@conditions) unless @conditions.empty?
81
+ params[:her_special_group] = MultiJson.dump(@group_conditions) unless @group_conditions.empty?
82
+ params[:her_special_having] = MultiJson.dump(@having_conditions) unless @having_conditions.empty?
83
+ params[:her_special_order] = MultiJson.dump(@order_conditions) unless @order_conditions.empty?
84
+ params[:her_special_limit] = @limit_value unless @limit_value.nil?
85
+ params[:her_special_offset] = @offset_value unless @offset_value.nil?
86
+ params[:her_special_paginate] = 1 if @do_paginate
87
+ params[:her_special_count] = 1 if @do_count
88
+
89
+ @model.request(params.merge(_method: :get, _path: @model.build_request_path(params))) do |parsed_data|
90
+ yield parsed_data if block_given?
91
+ end
92
+ end
93
+ end
94
+ end