her5 0.8.1
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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +1017 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
- data/her5.gemspec +30 -0
- data/lib/her.rb +19 -0
- data/lib/her/api.rb +120 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +104 -0
- data/lib/her/json_api/model.rb +57 -0
- data/lib/her/middleware.rb +12 -0
- data/lib/her/middleware/accept_json.rb +17 -0
- data/lib/her/middleware/first_level_parse_json.rb +36 -0
- data/lib/her/middleware/json_api_parser.rb +68 -0
- data/lib/her/middleware/parse_json.rb +28 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +75 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +107 -0
- data/lib/her/model/associations/association_proxy.rb +45 -0
- data/lib/her/model/associations/belongs_to_association.rb +101 -0
- data/lib/her/model/associations/has_many_association.rb +101 -0
- data/lib/her/model/associations/has_one_association.rb +80 -0
- data/lib/her/model/attributes.rb +297 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +113 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +84 -0
- data/lib/her/model/orm.rb +207 -0
- data/lib/her/model/parse.rb +221 -0
- data/lib/her/model/paths.rb +126 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +114 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/json_api/model_spec.rb +305 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/json_api_parser_spec.rb +32 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +31 -0
- data/spec/model/associations_spec.rb +504 -0
- data/spec/model/attributes_spec.rb +389 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +91 -0
- data/spec/model/http_spec.rb +158 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +506 -0
- data/spec/model/parse_spec.rb +345 -0
- data/spec/model/paths_spec.rb +347 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +44 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +289 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class Association
|
5
|
+
# @private
|
6
|
+
attr_accessor :params
|
7
|
+
|
8
|
+
# @private
|
9
|
+
def initialize(parent, opts = {})
|
10
|
+
@parent = parent
|
11
|
+
@opts = opts
|
12
|
+
@params = {}
|
13
|
+
|
14
|
+
@klass = @parent.class.her_nearby_class(@opts[:class_name])
|
15
|
+
@name = @opts[:name]
|
16
|
+
end
|
17
|
+
|
18
|
+
# @private
|
19
|
+
def self.proxy(parent, opts = {})
|
20
|
+
AssociationProxy.new new(parent, opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @private
|
24
|
+
def self.parse_single(association, klass, data)
|
25
|
+
data_key = association[:data_key]
|
26
|
+
return {} unless data[data_key]
|
27
|
+
|
28
|
+
klass = klass.her_nearby_class(association[:class_name])
|
29
|
+
if data[data_key].kind_of?(klass)
|
30
|
+
{ association[:name] => data[data_key] }
|
31
|
+
else
|
32
|
+
{ association[:name] => klass.new(klass.parse(data[data_key])) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @private
|
37
|
+
def assign_single_nested_attributes(attributes)
|
38
|
+
if @parent.attributes[@name].blank?
|
39
|
+
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
40
|
+
else
|
41
|
+
@parent.attributes[@name].assign_attributes(attributes)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @private
|
46
|
+
def fetch(opts = {})
|
47
|
+
attribute_value = @parent.attributes[@name]
|
48
|
+
return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
|
49
|
+
|
50
|
+
return @cached_result unless @params.any? || @cached_result.nil?
|
51
|
+
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
52
|
+
|
53
|
+
# No point trying to fetch associates for an object with no id yet.
|
54
|
+
if @parent.persisted?
|
55
|
+
path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
|
56
|
+
@klass.get(path, @params).tap do |result|
|
57
|
+
@cached_result = result unless @params.any?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
def build_association_path(code)
|
64
|
+
begin
|
65
|
+
instance_exec(&code)
|
66
|
+
rescue Her::Errors::PathError
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Add query parameters to the HTTP request performed to fetch the data
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# class User
|
75
|
+
# include Her::Model
|
76
|
+
# has_many :comments
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# user = User.find(1)
|
80
|
+
# user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
|
81
|
+
def where(params = {})
|
82
|
+
return self if params.blank? && @parent.attributes[@name].blank?
|
83
|
+
AssociationProxy.new self.clone.tap { |a| a.params = a.params.merge(params) }
|
84
|
+
end
|
85
|
+
alias all where
|
86
|
+
|
87
|
+
# Fetches the data specified by id
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# class User
|
91
|
+
# include Her::Model
|
92
|
+
# has_many :comments
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# user = User.find(1)
|
96
|
+
# user.comments.find(3) # Fetched via GET "/users/1/comments/3
|
97
|
+
#
|
98
|
+
def find(id)
|
99
|
+
return nil if id.blank?
|
100
|
+
path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
|
101
|
+
@klass.get_resource(path, @params)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class AssociationProxy < (ActiveSupport.const_defined?('ProxyObject') ? ActiveSupport::ProxyObject : ActiveSupport::BasicObject)
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def self.install_proxy_methods(target_name, *names)
|
8
|
+
names.each do |name|
|
9
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
10
|
+
def #{name}(*args, &block)
|
11
|
+
#{target_name}.send(#{name.inspect}, *args, &block)
|
12
|
+
end
|
13
|
+
RUBY
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
install_proxy_methods :association,
|
18
|
+
:build, :create, :where, :find, :all, :assign_nested_attributes
|
19
|
+
|
20
|
+
# @private
|
21
|
+
def initialize(association)
|
22
|
+
@_her_association = association
|
23
|
+
end
|
24
|
+
|
25
|
+
def association
|
26
|
+
@_her_association
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def method_missing(name, *args, &block)
|
31
|
+
if :object_id == name # avoid redefining object_id
|
32
|
+
return association.fetch.object_id
|
33
|
+
end
|
34
|
+
|
35
|
+
# create a proxy to the fetched object's method
|
36
|
+
AssociationProxy.install_proxy_methods 'association.fetch', name
|
37
|
+
|
38
|
+
# resend message to fetched object
|
39
|
+
__send__(name, *args, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class BelongsToAssociation < Association
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def self.attach(klass, name, opts)
|
8
|
+
opts = {
|
9
|
+
:class_name => name.to_s.classify,
|
10
|
+
:name => name,
|
11
|
+
:data_key => name,
|
12
|
+
:default => nil,
|
13
|
+
:foreign_key => "#{name}_id",
|
14
|
+
:path => "/#{name.to_s.pluralize}/:id"
|
15
|
+
}.merge(opts)
|
16
|
+
klass.associations[:belongs_to] << opts
|
17
|
+
|
18
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
def #{name}
|
20
|
+
cached_name = :"@_her_association_#{name}"
|
21
|
+
|
22
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
23
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.proxy(self, #{opts.inspect}))
|
24
|
+
end
|
25
|
+
RUBY
|
26
|
+
end
|
27
|
+
|
28
|
+
# @private
|
29
|
+
def self.parse(*args)
|
30
|
+
parse_single(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Initialize a new object
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# class User
|
37
|
+
# include Her::Model
|
38
|
+
# belongs_to :organization
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# class Organization
|
42
|
+
# include Her::Model
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# user = User.find(1)
|
46
|
+
# new_organization = user.organization.build(:name => "Foo Inc.")
|
47
|
+
# new_organization # => #<Organization name="Foo Inc.">
|
48
|
+
def build(attributes = {})
|
49
|
+
@klass.build(attributes)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a new object, save it and associate it to the parent
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# class User
|
56
|
+
# include Her::Model
|
57
|
+
# belongs_to :organization
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# class Organization
|
61
|
+
# include Her::Model
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# user = User.find(1)
|
65
|
+
# user.organization.create(:name => "Foo Inc.")
|
66
|
+
# user.organization # => #<Organization id=2 name="Foo Inc.">
|
67
|
+
def create(attributes = {})
|
68
|
+
resource = build(attributes)
|
69
|
+
@parent.attributes[@name] = resource if resource.save
|
70
|
+
resource
|
71
|
+
end
|
72
|
+
|
73
|
+
# @private
|
74
|
+
def fetch
|
75
|
+
foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
|
76
|
+
data_key_value = @parent.attributes[@opts[:data_key].to_sym]
|
77
|
+
return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (foreign_key_value.blank? && data_key_value.blank?)
|
78
|
+
|
79
|
+
return @cached_result unless @params.any? || @cached_result.nil?
|
80
|
+
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
81
|
+
|
82
|
+
path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
|
83
|
+
path = build_association_path lambda { @klass.build_request_path(path_params) }
|
84
|
+
begin
|
85
|
+
@klass.get_resource(path, @params).tap do |result|
|
86
|
+
@cached_result = result if @params.blank?
|
87
|
+
end
|
88
|
+
rescue Her::Errors::NotFound => e
|
89
|
+
@opts[:default].try(:dup)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @private
|
94
|
+
def assign_nested_attributes(attributes)
|
95
|
+
assign_single_nested_attributes(attributes)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class HasManyAssociation < Association
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def self.attach(klass, name, opts)
|
8
|
+
opts = {
|
9
|
+
:class_name => name.to_s.classify,
|
10
|
+
:name => name,
|
11
|
+
:data_key => name,
|
12
|
+
:default => Her::Collection.new,
|
13
|
+
:path => "/#{name}",
|
14
|
+
:inverse_of => nil
|
15
|
+
}.merge(opts)
|
16
|
+
klass.associations[:has_many] << opts
|
17
|
+
|
18
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
def #{name}
|
20
|
+
cached_name = :"@_her_association_#{name}"
|
21
|
+
|
22
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
23
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, #{opts.inspect}))
|
24
|
+
end
|
25
|
+
RUBY
|
26
|
+
end
|
27
|
+
|
28
|
+
# @private
|
29
|
+
def self.parse(association, klass, data)
|
30
|
+
data_key = association[:data_key]
|
31
|
+
return {} unless data[data_key]
|
32
|
+
|
33
|
+
klass = klass.her_nearby_class(association[:class_name])
|
34
|
+
{ association[:name] => Her::Model::Attributes.initialize_collection(klass, :data => data[data_key]) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Initialize a new object with a foreign key to the parent
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# class User
|
41
|
+
# include Her::Model
|
42
|
+
# has_many :comments
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class Comment
|
46
|
+
# include Her::Model
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# user = User.find(1)
|
50
|
+
# new_comment = user.comments.build(:body => "Hello!")
|
51
|
+
# new_comment # => #<Comment user_id=1 body="Hello!">
|
52
|
+
# TODO: This only merges the id of the parents, handle the case
|
53
|
+
# where this is more deeply nested
|
54
|
+
def build(attributes = {})
|
55
|
+
@klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a new object, save it and add it to the associated collection
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# class User
|
62
|
+
# include Her::Model
|
63
|
+
# has_many :comments
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# class Comment
|
67
|
+
# include Her::Model
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# user = User.find(1)
|
71
|
+
# user.comments.create(:body => "Hello!")
|
72
|
+
# user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
|
73
|
+
def create(attributes = {})
|
74
|
+
resource = build(attributes)
|
75
|
+
|
76
|
+
if resource.save
|
77
|
+
@parent.attributes[@name] ||= Her::Collection.new
|
78
|
+
@parent.attributes[@name] << resource
|
79
|
+
end
|
80
|
+
|
81
|
+
resource
|
82
|
+
end
|
83
|
+
|
84
|
+
# @private
|
85
|
+
def fetch
|
86
|
+
(super || []).tap do |o|
|
87
|
+
inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
|
88
|
+
o.each { |entry| entry.send("#{inverse_of}=", @parent) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @private
|
93
|
+
def assign_nested_attributes(attributes)
|
94
|
+
data = attributes.is_a?(Hash) ? attributes.values : attributes
|
95
|
+
@parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => data)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class HasOneAssociation < Association
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def self.attach(klass, name, opts)
|
8
|
+
opts = {
|
9
|
+
:class_name => name.to_s.classify,
|
10
|
+
:name => name,
|
11
|
+
:data_key => name,
|
12
|
+
:default => nil,
|
13
|
+
:path => "/#{name}"
|
14
|
+
}.merge(opts)
|
15
|
+
klass.associations[:has_one] << opts
|
16
|
+
|
17
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
18
|
+
def #{name}
|
19
|
+
cached_name = :"@_her_association_#{name}"
|
20
|
+
|
21
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
22
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.proxy(self, #{opts.inspect}))
|
23
|
+
end
|
24
|
+
RUBY
|
25
|
+
end
|
26
|
+
|
27
|
+
# @private
|
28
|
+
def self.parse(*args)
|
29
|
+
parse_single(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initialize a new object with a foreign key to the parent
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# class User
|
36
|
+
# include Her::Model
|
37
|
+
# has_one :role
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# class Role
|
41
|
+
# include Her::Model
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# user = User.find(1)
|
45
|
+
# new_role = user.role.build(:title => "moderator")
|
46
|
+
# new_role # => #<Role user_id=1 title="moderator">
|
47
|
+
def build(attributes = {})
|
48
|
+
@klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a new object, save it and associate it to the parent
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# class User
|
55
|
+
# include Her::Model
|
56
|
+
# has_one :role
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class Role
|
60
|
+
# include Her::Model
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# user = User.find(1)
|
64
|
+
# user.role.create(:title => "moderator")
|
65
|
+
# user.role # => #<Role id=2 user_id=1 title="moderator">
|
66
|
+
def create(attributes = {})
|
67
|
+
resource = build(attributes)
|
68
|
+
@parent.attributes[@name] = resource if resource.save
|
69
|
+
resource
|
70
|
+
end
|
71
|
+
|
72
|
+
# @private
|
73
|
+
def assign_nested_attributes(attributes)
|
74
|
+
assign_single_nested_attributes(attributes)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
# This module handles all methods related to model attributes
|
4
|
+
module Attributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Initialize a new object with data
|
8
|
+
#
|
9
|
+
# @param [Hash] attributes The attributes to initialize the object with
|
10
|
+
# @option attributes [Hash,Array] :_metadata
|
11
|
+
# @option attributes [Hash,Array] :_errors
|
12
|
+
# @option attributes [Boolean] :_destroyed
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class User
|
16
|
+
# include Her::Model
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# User.new(name: "Tobias") # => #<User name="Tobias">
|
20
|
+
def initialize(attributes={})
|
21
|
+
attributes ||= {}
|
22
|
+
@metadata = attributes.delete(:_metadata) || {}
|
23
|
+
@response_errors = attributes.delete(:_errors) || {}
|
24
|
+
@destroyed = attributes.delete(:_destroyed) || false
|
25
|
+
|
26
|
+
attributes = self.class.default_scope.apply_to(attributes)
|
27
|
+
assign_attributes(attributes)
|
28
|
+
run_callbacks :initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
# Initialize a collection of resources
|
32
|
+
#
|
33
|
+
# @private
|
34
|
+
def self.initialize_collection(klass, parsed_data={})
|
35
|
+
collection_data = klass.extract_array(parsed_data).map do |item_data|
|
36
|
+
if item_data.kind_of?(klass)
|
37
|
+
resource = item_data
|
38
|
+
else
|
39
|
+
resource = klass.new(klass.parse(item_data))
|
40
|
+
resource.run_callbacks :find
|
41
|
+
end
|
42
|
+
resource
|
43
|
+
end
|
44
|
+
Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
|
45
|
+
end
|
46
|
+
|
47
|
+
# Use setter methods of model for each key / value pair in params
|
48
|
+
# Return key / value pairs for which no setter method was defined on the model
|
49
|
+
#
|
50
|
+
# @private
|
51
|
+
def self.use_setter_methods(model, params)
|
52
|
+
params ||= {}
|
53
|
+
|
54
|
+
reserved_keys = [:id, model.class.primary_key] + model.class.association_keys
|
55
|
+
model.class.attributes *params.keys.reject { |k| reserved_keys.include?(k) || reserved_keys.map(&:to_s).include?(k) }
|
56
|
+
|
57
|
+
setter_method_names = model.class.setter_method_names
|
58
|
+
params.inject({}) do |memo, (key, value)|
|
59
|
+
setter_method = key.to_s + '='
|
60
|
+
if setter_method_names.include?(setter_method)
|
61
|
+
model.send(setter_method, value)
|
62
|
+
else
|
63
|
+
key = key.to_sym if key.is_a?(String)
|
64
|
+
memo[key] = value
|
65
|
+
end
|
66
|
+
memo
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Handles missing methods
|
71
|
+
#
|
72
|
+
# @private
|
73
|
+
def method_missing(method, *args, &blk)
|
74
|
+
if method.to_s =~ /[?=]$/ || @attributes.include?(method)
|
75
|
+
# Extract the attribute
|
76
|
+
attribute = method.to_s.sub(/[?=]$/, '')
|
77
|
+
|
78
|
+
# Create a new `attribute` methods set
|
79
|
+
self.class.attributes(*attribute)
|
80
|
+
|
81
|
+
# Resend the method!
|
82
|
+
send(method, *args, &blk)
|
83
|
+
else
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @private
|
89
|
+
def respond_to_missing?(method, include_private = false)
|
90
|
+
method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
|
91
|
+
end
|
92
|
+
|
93
|
+
# Assign new attributes to a resource
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# class User
|
97
|
+
# include Her::Model
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# user = User.find(1) # => #<User id=1 name="Tobias">
|
101
|
+
# user.assign_attributes(name: "Lindsay")
|
102
|
+
# user.changes # => { :name => ["Tobias", "Lindsay"] }
|
103
|
+
def assign_attributes(new_attributes)
|
104
|
+
@attributes ||= attributes
|
105
|
+
# Use setter methods first
|
106
|
+
unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
|
107
|
+
|
108
|
+
# Then translate attributes of associations into association instances
|
109
|
+
parsed_attributes = self.class.parse_associations(unset_attributes)
|
110
|
+
|
111
|
+
# Then merge the parsed_data into @attributes.
|
112
|
+
@attributes.merge!(parsed_attributes)
|
113
|
+
end
|
114
|
+
alias attributes= assign_attributes
|
115
|
+
|
116
|
+
def attributes
|
117
|
+
@attributes ||= HashWithIndifferentAccess.new
|
118
|
+
end
|
119
|
+
|
120
|
+
# Handles returning true for the accessible attributes
|
121
|
+
#
|
122
|
+
# @private
|
123
|
+
def has_attribute?(attribute_name)
|
124
|
+
@attributes.include?(attribute_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Handles returning data for a specific attribute
|
128
|
+
#
|
129
|
+
# @private
|
130
|
+
def get_attribute(attribute_name)
|
131
|
+
@attributes[attribute_name]
|
132
|
+
end
|
133
|
+
alias attribute get_attribute
|
134
|
+
|
135
|
+
# Return the value of the model `primary_key` attribute
|
136
|
+
def id
|
137
|
+
@attributes[self.class.primary_key]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Mark as destroyed in order to relay _destroy parameter when nested
|
141
|
+
def _destroy=(value)
|
142
|
+
@destroying = !!value
|
143
|
+
end
|
144
|
+
|
145
|
+
def _destroy
|
146
|
+
@destroying ||= false
|
147
|
+
end
|
148
|
+
|
149
|
+
def destroying?
|
150
|
+
@destroying || false
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return `true` if the other object is also a Her::Model and has matching data
|
154
|
+
#
|
155
|
+
# @private
|
156
|
+
def ==(other)
|
157
|
+
other.is_a?(Her::Model) && @attributes == other.attributes
|
158
|
+
end
|
159
|
+
|
160
|
+
# Delegate to the == method
|
161
|
+
#
|
162
|
+
# @private
|
163
|
+
def eql?(other)
|
164
|
+
self == other
|
165
|
+
end
|
166
|
+
|
167
|
+
# Delegate to @attributes, allowing models to act correctly in code like:
|
168
|
+
# [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
|
169
|
+
# @private
|
170
|
+
def hash
|
171
|
+
@attributes.hash
|
172
|
+
end
|
173
|
+
|
174
|
+
# Assign attribute value (ActiveModel convention method).
|
175
|
+
#
|
176
|
+
# @private
|
177
|
+
def attribute=(attribute, value)
|
178
|
+
@attributes[attribute] = nil unless @attributes.include?(attribute)
|
179
|
+
self.send(:"#{attribute}_will_change!") if @attributes[attribute] != value
|
180
|
+
@attributes[attribute] = value
|
181
|
+
end
|
182
|
+
|
183
|
+
# Check attribute value to be present (ActiveModel convention method).
|
184
|
+
#
|
185
|
+
# @private
|
186
|
+
def attribute?(attribute)
|
187
|
+
@attributes.include?(attribute) && @attributes[attribute].present?
|
188
|
+
end
|
189
|
+
|
190
|
+
module ClassMethods
|
191
|
+
# Initialize a collection of resources with raw data from an HTTP request
|
192
|
+
#
|
193
|
+
# @param [Array] parsed_data
|
194
|
+
# @private
|
195
|
+
def new_collection(parsed_data)
|
196
|
+
Her::Model::Attributes.initialize_collection(self, parsed_data)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Initialize a new object with the "raw" parsed_data from the parsing middleware
|
200
|
+
#
|
201
|
+
# @private
|
202
|
+
def new_from_parsed_data(parsed_data)
|
203
|
+
parsed_data = parsed_data.with_indifferent_access
|
204
|
+
new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
|
205
|
+
end
|
206
|
+
|
207
|
+
# Define attribute method matchers to automatically define them using ActiveModel's define_attribute_methods.
|
208
|
+
#
|
209
|
+
# @private
|
210
|
+
def define_attribute_method_matchers
|
211
|
+
attribute_method_suffix '='
|
212
|
+
attribute_method_suffix '?'
|
213
|
+
end
|
214
|
+
|
215
|
+
# Create a mutex for dynamically generated attribute methods or use one defined by ActiveModel.
|
216
|
+
#
|
217
|
+
# @private
|
218
|
+
def attribute_methods_mutex
|
219
|
+
@attribute_methods_mutex ||= if generated_attribute_methods.respond_to? :mu_synchronize
|
220
|
+
generated_attribute_methods
|
221
|
+
else
|
222
|
+
Mutex.new
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Define the attributes that will be used to track dirty attributes and validations
|
227
|
+
#
|
228
|
+
# @param [Array] attributes
|
229
|
+
# @example
|
230
|
+
# class User
|
231
|
+
# include Her::Model
|
232
|
+
# attributes :name, :email
|
233
|
+
# end
|
234
|
+
def attributes(*attributes)
|
235
|
+
attribute_methods_mutex.synchronize do
|
236
|
+
define_attribute_methods attributes
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
|
241
|
+
#
|
242
|
+
# @param [Symbol] store_response_errors
|
243
|
+
#
|
244
|
+
# @example
|
245
|
+
# class User
|
246
|
+
# include Her::Model
|
247
|
+
# store_response_errors :server_errors
|
248
|
+
# end
|
249
|
+
def store_response_errors(value = nil)
|
250
|
+
store_her_data(:response_errors, value)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
|
254
|
+
#
|
255
|
+
# @param [Symbol] store_metadata
|
256
|
+
#
|
257
|
+
# @example
|
258
|
+
# class User
|
259
|
+
# include Her::Model
|
260
|
+
# store_metadata :server_data
|
261
|
+
# end
|
262
|
+
def store_metadata(value = nil)
|
263
|
+
store_her_data(:metadata, value)
|
264
|
+
end
|
265
|
+
|
266
|
+
# @private
|
267
|
+
def setter_method_names
|
268
|
+
@_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
|
269
|
+
memo << method_name.to_s if method_name.to_s.end_with?('=')
|
270
|
+
memo
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
# @private
|
276
|
+
def store_her_data(name, value)
|
277
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
278
|
+
if @_her_store_#{name} && value.present?
|
279
|
+
remove_method @_her_store_#{name}.to_sym
|
280
|
+
remove_method @_her_store_#{name}.to_s + '='
|
281
|
+
end
|
282
|
+
|
283
|
+
@_her_store_#{name} ||= begin
|
284
|
+
superclass.store_#{name} if superclass.respond_to?(:store_#{name})
|
285
|
+
end
|
286
|
+
|
287
|
+
return @_her_store_#{name} unless value
|
288
|
+
@_her_store_#{name} = value
|
289
|
+
|
290
|
+
define_method(value) { @#{name} }
|
291
|
+
define_method(value.to_s+'=') { |value| @#{name} = value }
|
292
|
+
RUBY
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|