her 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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