her 0.2.5 → 0.2.6

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.
@@ -0,0 +1,12 @@
1
+ module Her
2
+ class Collection < ::Array
3
+ attr_reader :metadata, :errors
4
+
5
+ # @private
6
+ def initialize(items=[], metadata={}, errors=[]) # {{{
7
+ super(items)
8
+ @metadata = metadata || {}
9
+ @errors = errors || []
10
+ end # }}}
11
+ end
12
+ end
@@ -1,8 +1,9 @@
1
+ require "her/middleware/first_level_parse_json"
2
+ require "her/middleware/second_level_parse_json"
3
+ require "her/middleware/accept_json"
4
+
1
5
  module Her
2
6
  module Middleware
3
- autoload :FirstLevelParseJSON, "her/middleware/first_level_parse_json"
4
- autoload :SecondLevelParseJSON, "her/middleware/second_level_parse_json"
5
-
6
7
  DefaultParseJSON = FirstLevelParseJSON
7
8
  end
8
9
  end
@@ -0,0 +1,15 @@
1
+ module Her
2
+ module Middleware
3
+ # This middleware adds a "Accept: application/json" HTTP header
4
+ class AcceptJSON < Faraday::Middleware
5
+ def add_header(headers) # {{{
6
+ headers.merge! "Accept" => "application/json"
7
+ end # }}}
8
+
9
+ def call(env) # {{{
10
+ add_header(env[:request_headers])
11
+ @app.call(env)
12
+ end # }}}
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,11 @@
1
+ require "her/model/base"
2
+ require "her/model/http"
3
+ require "her/model/orm"
4
+ require "her/model/relationships"
5
+ require "her/model/hooks"
6
+ require "her/model/introspection"
7
+ require "her/model/paths"
8
+
1
9
  module Her
2
10
  # This module is the main element of Her. After creating a Her::API object,
3
11
  # include this module in your models to get a few magic methods defined in them.
@@ -10,14 +18,6 @@ module Her
10
18
  # @user = User.new(:name => "Rémi")
11
19
  # @user.save
12
20
  module Model
13
- autoload :Base, "her/model/base"
14
- autoload :HTTP, "her/model/http"
15
- autoload :ORM, "her/model/orm"
16
- autoload :Relationships, "her/model/relationships"
17
- autoload :Hooks, "her/model/hooks"
18
- autoload :Introspection, "her/model/introspection"
19
- autoload :Paths, "her/model/paths"
20
-
21
21
  extend ActiveSupport::Concern
22
22
 
23
23
  # Instance methods
@@ -51,6 +51,14 @@ module Her
51
51
  # @param [Symbol, &block] method A method or a block to be called
52
52
  def after_destroy(method=nil, &block); set_hook(:after, :destroy, method || block); end
53
53
 
54
+ # Wrap a block between “before” and “after” hooks
55
+ # @private
56
+ def wrap_in_hooks(resource, *hooks) # {{{
57
+ perform_before_hooks(resource, *hooks)
58
+ yield(resource, resource.class)
59
+ perform_after_hooks(resource, *hooks.reverse)
60
+ end # }}}
61
+
54
62
  private
55
63
  # @private
56
64
  def hooks # {{{
@@ -75,6 +83,22 @@ module Her
75
83
  end
76
84
  end
77
85
  end # }}}
86
+
87
+ # Perform “after” hooks on a resource
88
+ # @private
89
+ def perform_after_hooks(resource, *hooks) # {{{
90
+ hooks.each do |hook|
91
+ perform_hook(resource, :after, hook)
92
+ end
93
+ end # }}}
94
+
95
+ # Perform “before” hooks on a resource
96
+ # @private
97
+ def perform_before_hooks(resource, *hooks) # {{{
98
+ hooks.each do |hook|
99
+ perform_hook(resource, :before, hook)
100
+ end
101
+ end # }}}
78
102
  end
79
103
  end
80
104
  end
@@ -26,9 +26,9 @@ module Her
26
26
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
27
27
  get_raw(path, attrs) do |parsed_data|
28
28
  if parsed_data[:data].is_a?(Array)
29
- new_collection(parsed_data[:data])
29
+ new_collection(parsed_data)
30
30
  else
31
- new(parsed_data[:data])
31
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
32
32
  end
33
33
  end
34
34
  end # }}}
@@ -43,7 +43,7 @@ module Her
43
43
  def get_collection(path, attrs={}) # {{{
44
44
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
45
45
  get_raw(path, attrs) do |parsed_data|
46
- new_collection(parsed_data[:data])
46
+ new_collection(parsed_data)
47
47
  end
48
48
  end # }}}
49
49
 
@@ -51,7 +51,7 @@ module Her
51
51
  def get_resource(path, attrs={}) # {{{
52
52
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
53
53
  get_raw(path, attrs) do |parsed_data|
54
- new(parsed_data[:data])
54
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
55
55
  end
56
56
  end # }}}
57
57
 
@@ -60,9 +60,9 @@ module Her
60
60
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
61
61
  post_raw(path, attrs) do |parsed_data|
62
62
  if parsed_data[:data].is_a?(Array)
63
- new_collection(parsed_data[:data])
63
+ new_collection(parsed_data)
64
64
  else
65
- new(parsed_data[:data])
65
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
66
66
  end
67
67
  end
68
68
  end # }}}
@@ -77,7 +77,7 @@ module Her
77
77
  def post_collection(path, attrs={}) # {{{
78
78
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
79
79
  post_raw(path, attrs) do |parsed_data|
80
- new_collection(parsed_data[:data])
80
+ new_collection(parsed_data)
81
81
  end
82
82
  end # }}}
83
83
 
@@ -94,9 +94,9 @@ module Her
94
94
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
95
95
  put_raw(path, attrs) do |parsed_data|
96
96
  if parsed_data[:data].is_a?(Array)
97
- new_collection(parsed_data[:data])
97
+ new_collection(parsed_data)
98
98
  else
99
- new(parsed_data[:data])
99
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
100
100
  end
101
101
  end
102
102
  end # }}}
@@ -111,7 +111,7 @@ module Her
111
111
  def put_collection(path, attrs={}) # {{{
112
112
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
113
113
  put_raw(path, attrs) do |parsed_data|
114
- new_collection(parsed_data[:data])
114
+ new_collection(parsed_data)
115
115
  end
116
116
  end # }}}
117
117
 
@@ -119,7 +119,7 @@ module Her
119
119
  def put_resource(path, attrs={}) # {{{
120
120
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
121
121
  put_raw(path, attrs) do |parsed_data|
122
- new(parsed_data[:data])
122
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
123
123
  end
124
124
  end # }}}
125
125
 
@@ -128,9 +128,9 @@ module Her
128
128
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
129
129
  patch_raw(path, attrs) do |parsed_data|
130
130
  if parsed_data[:data].is_a?(Array)
131
- new_collection(parsed_data[:data])
131
+ new_collection(parsed_data)
132
132
  else
133
- new(parsed_data[:data])
133
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
134
134
  end
135
135
  end
136
136
  end # }}}
@@ -145,7 +145,7 @@ module Her
145
145
  def patch_collection(path, attrs={}) # {{{
146
146
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
147
147
  patch_raw(path, attrs) do |parsed_data|
148
- new_collection(parsed_data[:data])
148
+ new_collection(parsed_data)
149
149
  end
150
150
  end # }}}
151
151
 
@@ -153,7 +153,7 @@ module Her
153
153
  def patch_resource(path, attrs={}) # {{{
154
154
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
155
155
  patch_raw(path, attrs) do |parsed_data|
156
- new(parsed_data[:data])
156
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
157
157
  end
158
158
  end # }}}
159
159
 
@@ -162,9 +162,9 @@ module Her
162
162
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
163
163
  delete_raw(path, attrs) do |parsed_data|
164
164
  if parsed_data[:data].is_a?(Array)
165
- new_collection(parsed_data[:data])
165
+ new_collection(parsed_data)
166
166
  else
167
- new(parsed_data[:data])
167
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
168
168
  end
169
169
  end
170
170
  end # }}}
@@ -179,7 +179,7 @@ module Her
179
179
  def delete_collection(path, attrs={}) # {{{
180
180
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
181
181
  delete_raw(path, attrs) do |parsed_data|
182
- new_collection(parsed_data[:data])
182
+ new_collection(parsed_data)
183
183
  end
184
184
  end # }}}
185
185
 
@@ -187,7 +187,7 @@ module Her
187
187
  def delete_resource(path, attrs={}) # {{{
188
188
  path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol)
189
189
  delete_raw(path, attrs) do |parsed_data|
190
- new(parsed_data[:data])
190
+ new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors])
191
191
  end
192
192
  end # }}}
193
193
 
@@ -2,17 +2,29 @@ module Her
2
2
  module Model
3
3
  # This module adds ORM-like capabilities to the model
4
4
  module ORM
5
+ attr_reader :metadata, :errors
6
+
5
7
  # Initialize a new object with data received from an HTTP request
6
8
  # @private
7
- def initialize(single_data) # {{{
8
- @data = single_data
9
- @data = self.class.parse_relationships(@data)
9
+ def initialize(data={}) # {{{
10
+ @data = {}
11
+ @metadata = data.delete(:_metadata) || {}
12
+ @errors = data.delete(:_errors) || {}
13
+ cleaned_data = data.inject({}) do |memo, item|
14
+ key, value = item
15
+ send "#{key}=".to_sym, value unless value.nil?
16
+ respond_to?("#{key}=") ? memo : memo.merge({ key => value })
17
+ end
18
+ @data.merge! self.class.parse_relationships(cleaned_data)
10
19
  end # }}}
11
20
 
12
21
  # Initialize a collection of resources
13
22
  # @private
14
- def self.initialize_collection(name, collection_data) # {{{
15
- collection_data.map { |item_data| Object.const_get(name.to_s.classify).new(item_data) }
23
+ def self.initialize_collection(name, parsed_data={}) # {{{
24
+ collection_data = parsed_data[:data].map do |item_data|
25
+ Object.const_get(name.to_s.classify).new(item_data)
26
+ end
27
+ Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
16
28
  end # }}}
17
29
 
18
30
  # Handles missing methods by routing them through @data
@@ -20,10 +32,11 @@ module Her
20
32
  def method_missing(method, attrs=nil) # {{{
21
33
  assignment_method = method.to_s =~ /\=$/
22
34
  method = method.to_s.gsub(/(\?|\!|\=)$/, "").to_sym
23
- if attrs and assignment_method
35
+ if !attrs.nil? and assignment_method
36
+ @data ||= {}
24
37
  @data[method.to_s.gsub(/\=$/, "").to_sym] = attrs
25
38
  else
26
- if @data.include?(method)
39
+ if @data and @data.include?(method)
27
40
  @data[method]
28
41
  else
29
42
  super
@@ -39,9 +52,9 @@ module Her
39
52
 
40
53
  # Initialize a collection of resources with raw data from an HTTP request
41
54
  #
42
- # @param [Array] collection_data An array of model hashes
43
- def new_collection(collection_data) # {{{
44
- Her::Model::ORM.initialize_collection(self.to_s.underscore, collection_data)
55
+ # @param [Array] parsed_data
56
+ def new_collection(parsed_data) # {{{
57
+ Her::Model::ORM.initialize_collection(self.to_s.underscore, parsed_data)
45
58
  end # }}}
46
59
 
47
60
  # Return `true` if a resource was not saved yet
@@ -49,6 +62,16 @@ module Her
49
62
  !@data.include?(:id)
50
63
  end # }}}
51
64
 
65
+ # Return `true` if a resource does not contain errors
66
+ def valid? # {{{
67
+ @errors.empty?
68
+ end # }}}
69
+
70
+ # Return `true` if a resource contains errors
71
+ def invalid? # {{{
72
+ @errors.any?
73
+ end # }}}
74
+
52
75
  # Fetch a specific resource based on an ID
53
76
  #
54
77
  # @example
@@ -67,7 +90,7 @@ module Her
67
90
  # # Fetched via GET "/users"
68
91
  def all(params={}) # {{{
69
92
  request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data|
70
- new_collection(parsed_data[:data])
93
+ new_collection(parsed_data)
71
94
  end
72
95
  end # }}}
73
96
 
@@ -78,17 +101,16 @@ module Her
78
101
  # # Called via POST "/users/1"
79
102
  def create(params={}) # {{{
80
103
  resource = new(params)
81
- perform_hook(resource, :before, :create)
82
- perform_hook(resource, :before, :save)
83
- params = resource.instance_eval { @data }
84
- request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data|
85
- resource.instance_eval do
86
- @data = parsed_data[:data]
104
+ wrap_in_hooks(resource, :create, :save) do |resource, klass|
105
+ params = resource.instance_eval { @data }
106
+ request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data|
107
+ resource.instance_eval do
108
+ @data = parsed_data[:data]
109
+ @metadata = parsed_data[:metadata]
110
+ @errors = parsed_data[:errors]
111
+ end
87
112
  end
88
113
  end
89
- perform_hook(resource, :after, :save)
90
- perform_hook(resource, :after, :create)
91
-
92
114
  resource
93
115
  end # }}}
94
116
 
@@ -118,30 +140,20 @@ module Her
118
140
  def save # {{{
119
141
  params = @data.dup
120
142
  resource = self
143
+
121
144
  if @data[:id]
122
- self.class.class_eval do
123
- perform_hook(resource, :before, :update)
124
- perform_hook(resource, :before, :save)
125
- end
126
- self.class.request(params.merge(:_method => :put, :_path => "#{request_path}")) do |parsed_data|
127
- @data = parsed_data[:data]
128
- end
129
- self.class.class_eval do
130
- perform_hook(resource, :after, :save)
131
- perform_hook(resource, :after, :update)
132
- end
133
- self
145
+ hooks = [:update, :save]
146
+ method = :put
134
147
  else
135
- self.class.class_eval do
136
- perform_hook(resource, :before, :create)
137
- perform_hook(resource, :before, :save)
138
- end
139
- self.class.request(params.merge(:_method => :post, :_path => "#{request_path}")) do |parsed_data|
148
+ hooks = [:create, :save]
149
+ method = :post
150
+ end
151
+
152
+ self.class.wrap_in_hooks(resource, *hooks) do |resource, klass|
153
+ klass.request(params.merge(:_method => method, :_path => "#{request_path}")) do |parsed_data|
140
154
  @data = parsed_data[:data]
141
- end
142
- self.class.class_eval do
143
- perform_hook(resource, :after, :save)
144
- perform_hook(resource, :after, :create)
155
+ @metadata = parsed_data[:metadata]
156
+ @errors = parsed_data[:errors]
145
157
  end
146
158
  end
147
159
  self
@@ -154,13 +166,14 @@ module Her
154
166
  # @user.destroy
155
167
  # # Called via DELETE "/users/1"
156
168
  def destroy # {{{
157
- params = @data.dup
158
169
  resource = self
159
- self.class.class_eval { perform_hook(resource, :before, :destroy) }
160
- self.class.request(params.merge(:_method => :delete, :_path => "#{request_path}")) do |parsed_data|
161
- @data = parsed_data[:data]
170
+ self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
171
+ klass.request(:_method => :delete, :_path => "#{request_path}") do |parsed_data|
172
+ @data = parsed_data[:data]
173
+ @metadata = parsed_data[:metadata]
174
+ @errors = parsed_data[:errors]
175
+ end
162
176
  end
163
- self.class.class_eval { perform_hook(resource, :after, :destroy) }
164
177
  self
165
178
  end # }}}
166
179
 
@@ -14,14 +14,16 @@ module Her
14
14
  @her_relationships ||= {}
15
15
  @her_relationships.each_pair do |type, relationships|
16
16
  relationships.each do |relationship|
17
- if data.include?(relationship[:name])
18
- if type == :has_many
19
- data[relationship[:name]] = Her::Model::ORM.initialize_collection(relationship[:class_name], data[relationship[:name]])
20
- elsif type == :has_one
21
- data[relationship[:name]] = Object.const_get(relationship[:class_name]).new(data[relationship[:name]])
22
- elsif type == :belongs_to
23
- data[relationship[:name]] = Object.const_get(relationship[:class_name]).new(data[relationship[:name]])
24
- end
17
+ name = relationship[:name]
18
+ class_name = relationship[:class_name]
19
+ next if !data.include?(name) or data[name].nil?
20
+ data[name] = case type
21
+ when :has_many
22
+ Her::Model::ORM.initialize_collection(class_name, :data => data[name])
23
+ when :has_one, :belongs_to
24
+ Object.const_get(class_name).new(data[name])
25
+ else
26
+ nil
25
27
  end
26
28
  end
27
29
  end
@@ -47,14 +49,8 @@ module Her
47
49
  # @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
48
50
  # # Fetched via GET "/users/1/articles"
49
51
  def has_many(name, attrs={}) # {{{
50
- @her_relationships ||= {}
51
52
  attrs = { :class_name => name.to_s.classify, :name => name }.merge(attrs)
52
- (@her_relationships[:has_many] ||= []) << attrs
53
-
54
- define_method(name) do
55
- return @data[name] if @data.include?(name) # Do not fetch from API again if we have it in @data
56
- Object.const_get(attrs[:class_name]).get_collection("#{self.class.build_request_path(:id => id)}/#{name.to_s.pluralize}")
57
- end
53
+ define_relationship(:has_many, attrs)
58
54
  end # }}}
59
55
 
60
56
  # Define an *has_one* relationship.
@@ -76,14 +72,8 @@ module Her
76
72
  # @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
77
73
  # # Fetched via GET "/users/1/organization"
78
74
  def has_one(name, attrs={}) # {{{
79
- @her_relationships ||= {}
80
- attrs = { :class_name => name.to_s.classify, :name => name, :foreign_key => "#{name}_id" }.merge(attrs)
81
- (@her_relationships[:has_one] ||= []) << attrs
82
-
83
- define_method(name) do
84
- return @data[name] if @data.include?(name) # Do not fetch from API again if we have it in @data
85
- Object.const_get(attrs[:class_name]).get_resource("#{self.class.build_request_path(:id => id)}/#{name.to_s.singularize}")
86
- end
75
+ attrs = { :class_name => name.to_s.classify, :name => name }.merge(attrs)
76
+ define_relationship(:has_one, attrs)
87
77
  end # }}}
88
78
 
89
79
  # Define a *belongs_to* relationship.
@@ -105,13 +95,36 @@ module Her
105
95
  # @user.team # => #<Team(teams/2) id=2 name="Developers">
106
96
  # # Fetched via GET "/teams/2"
107
97
  def belongs_to(name, attrs={}) # {{{
108
- @her_relationships ||= {}
109
98
  attrs = { :class_name => name.to_s.classify, :name => name, :foreign_key => "#{name}_id" }.merge(attrs)
110
- (@her_relationships[:belongs_to] ||= []) << attrs
99
+ define_relationship(:belongs_to, attrs)
100
+ end # }}}
111
101
 
102
+ private
103
+ # @private
104
+ def define_relationship(type, attrs) # {{{
105
+ @her_relationships ||= {}
106
+ (@her_relationships[type] ||= []) << attrs
107
+ relationship_accessor(type, attrs)
108
+ end # }}}
109
+
110
+ # @private
111
+ def relationship_accessor(type, attrs) # {{{
112
+ name = attrs[:name]
113
+ class_name = attrs[:class_name]
112
114
  define_method(name) do
113
- return @data[name] if @data.include?(name) # Do not fetch from API again if we have it in @data
114
- Object.const_get(attrs[:class_name]).get_resource("#{Object.const_get(name.to_s.classify).build_request_path(:id => @data["#{name}_id".to_sym])}")
115
+ return @data[name] if @data.include?(name)
116
+
117
+ klass = Object.const_get(class_name)
118
+ path = self.class.build_request_path(:id => id)
119
+ @data[name] = case type
120
+ when :belongs_to
121
+ foreign_key = attrs[:foreign_key].to_sym
122
+ klass.get_resource("#{klass.build_request_path(:id => @data[foreign_key])}")
123
+ when :has_many
124
+ klass.get_collection("#{path}/#{name.to_s.pluralize}")
125
+ when :has_one
126
+ klass.get_resource("#{path}/#{name.to_s.singularize}")
127
+ end
115
128
  end
116
129
  end # }}}
117
130
  end