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