him 0.1.0
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/.github/workflows/ci.yml +40 -0
- data/.gitignore +6 -0
- data/.qlty/qlty.toml +57 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/LICENSE +8 -0
- data/README.md +1007 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-6.1 +6 -0
- data/gemfiles/Gemfile.activemodel-7.0 +6 -0
- data/gemfiles/Gemfile.activemodel-7.1 +6 -0
- data/gemfiles/Gemfile.activemodel-7.2 +6 -0
- data/gemfiles/Gemfile.activemodel-8.0 +6 -0
- data/him.gemspec +28 -0
- data/lib/him/api.rb +121 -0
- data/lib/him/collection.rb +21 -0
- data/lib/him/errors.rb +29 -0
- data/lib/him/json_api/model.rb +42 -0
- data/lib/him/middleware/accept_json.rb +18 -0
- data/lib/him/middleware/first_level_parse_json.rb +37 -0
- data/lib/him/middleware/json_api_parser.rb +65 -0
- data/lib/him/middleware/parse_json.rb +22 -0
- data/lib/him/middleware/second_level_parse_json.rb +37 -0
- data/lib/him/middleware.rb +12 -0
- data/lib/him/model/associations/association.rb +147 -0
- data/lib/him/model/associations/association_proxy.rb +47 -0
- data/lib/him/model/associations/belongs_to_association.rb +95 -0
- data/lib/him/model/associations/has_many_association.rb +113 -0
- data/lib/him/model/associations/has_one_association.rb +79 -0
- data/lib/him/model/associations.rb +141 -0
- data/lib/him/model/attributes.rb +337 -0
- data/lib/him/model/base.rb +33 -0
- data/lib/him/model/http.rb +113 -0
- data/lib/him/model/introspection.rb +77 -0
- data/lib/him/model/nested_attributes.rb +45 -0
- data/lib/him/model/orm.rb +306 -0
- data/lib/him/model/parse.rb +224 -0
- data/lib/him/model/paths.rb +125 -0
- data/lib/him/model/relation.rb +212 -0
- data/lib/him/model.rb +79 -0
- data/lib/him/version.rb +3 -0
- data/lib/him.rb +22 -0
- data/spec/api_spec.rb +120 -0
- data/spec/collection_spec.rb +70 -0
- data/spec/json_api/model_spec.rb +260 -0
- data/spec/middleware/accept_json_spec.rb +11 -0
- data/spec/middleware/first_level_parse_json_spec.rb +63 -0
- data/spec/middleware/json_api_parser_spec.rb +52 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +29 -0
- data/spec/model/associations_spec.rb +1010 -0
- data/spec/model/attributes_spec.rb +384 -0
- data/spec/model/callbacks_spec.rb +194 -0
- data/spec/model/dirty_spec.rb +133 -0
- data/spec/model/http_spec.rb +187 -0
- data/spec/model/introspection_spec.rb +110 -0
- data/spec/model/nested_attributes_spec.rb +135 -0
- data/spec/model/orm_spec.rb +717 -0
- data/spec/model/parse_spec.rb +619 -0
- data/spec/model/paths_spec.rb +348 -0
- data/spec/model/relation_spec.rb +255 -0
- data/spec/model/validations_spec.rb +45 -0
- data/spec/model_spec.rb +55 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/extensions/array.rb +6 -0
- data/spec/support/extensions/hash.rb +6 -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 +201 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
module Associations
|
|
4
|
+
class Association
|
|
5
|
+
|
|
6
|
+
# @private
|
|
7
|
+
attr_accessor :params, :cached_result
|
|
8
|
+
|
|
9
|
+
# @private
|
|
10
|
+
def initialize(parent, opts = {})
|
|
11
|
+
@parent = parent
|
|
12
|
+
@opts = opts
|
|
13
|
+
@params = {}
|
|
14
|
+
|
|
15
|
+
@klass = @parent.class.her_nearby_class(@opts[:class_name])
|
|
16
|
+
@name = @opts[:name]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the foreign key to use when building child records,
|
|
20
|
+
# respecting the :foreign_key option or falling back to convention.
|
|
21
|
+
def foreign_key
|
|
22
|
+
@opts[:foreign_key]&.to_sym || :"#{@parent.singularized_resource_name}_id"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @private
|
|
26
|
+
def self.proxy(parent, opts = {})
|
|
27
|
+
AssociationProxy.new new(parent, opts)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @private
|
|
31
|
+
def self.parse_single(association, klass, data)
|
|
32
|
+
data_key = association[:data_key]
|
|
33
|
+
return {} unless data[data_key]
|
|
34
|
+
|
|
35
|
+
klass = klass.her_nearby_class(association[:class_name])
|
|
36
|
+
{ association[:name] => klass.instantiate_record(klass, data: data[data_key]) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @private
|
|
40
|
+
def assign_single_nested_attributes(attributes)
|
|
41
|
+
if @parent.attributes[@name].blank?
|
|
42
|
+
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
|
43
|
+
else
|
|
44
|
+
@parent.attributes[@name].assign_attributes(attributes)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @private
|
|
49
|
+
def fetch(opts = {})
|
|
50
|
+
attribute_value = @parent.attributes[@name]
|
|
51
|
+
return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
|
|
52
|
+
|
|
53
|
+
return @cached_result unless @params.any? || @cached_result.nil?
|
|
54
|
+
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
|
55
|
+
return @opts[:default].try(:dup) if @parent.new?
|
|
56
|
+
|
|
57
|
+
if @params.values.include?([]) && !is_a?(HasOneAssociation)
|
|
58
|
+
Him::Collection.new
|
|
59
|
+
else
|
|
60
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}" }
|
|
61
|
+
request_association(path, @params).tap do |result|
|
|
62
|
+
@cached_result = result unless @params.any?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @private
|
|
68
|
+
def request_association(path, params)
|
|
69
|
+
if @opts[:default].is_a?(Array)
|
|
70
|
+
@klass.get_collection(path, params)
|
|
71
|
+
else
|
|
72
|
+
@klass.get(path, params)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @private
|
|
77
|
+
def build_association_path(code)
|
|
78
|
+
instance_exec(&code)
|
|
79
|
+
rescue Him::Errors::PathError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @private
|
|
84
|
+
def reset
|
|
85
|
+
@params = {}
|
|
86
|
+
@cached_result = nil
|
|
87
|
+
@parent.attributes.delete(@name)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Add query parameters to the HTTP request performed to fetch the data
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# class User
|
|
94
|
+
# include Him::Model
|
|
95
|
+
# has_many :comments
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# user = User.find(1)
|
|
99
|
+
# user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
|
|
100
|
+
def where(params = {})
|
|
101
|
+
return self if params.blank? && @parent.attributes[@name].blank?
|
|
102
|
+
AssociationProxy.new clone.tap { |a| a.params = a.params.merge(params) }
|
|
103
|
+
end
|
|
104
|
+
alias all where
|
|
105
|
+
|
|
106
|
+
# Fetches the data specified by id
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# class User
|
|
110
|
+
# include Him::Model
|
|
111
|
+
# has_many :comments
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# user = User.find(1)
|
|
115
|
+
# user.comments.find(3) # Fetched via GET "/users/1/comments/3
|
|
116
|
+
def find(id)
|
|
117
|
+
return nil if id.blank?
|
|
118
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
|
|
119
|
+
@klass.get_resource(path, @params)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Refetches the association and puts the proxy back in its initial state,
|
|
123
|
+
# which is unloaded. Cached associations are cleared.
|
|
124
|
+
#
|
|
125
|
+
# @example
|
|
126
|
+
# class User
|
|
127
|
+
# include Him::Model
|
|
128
|
+
# has_many :comments
|
|
129
|
+
# end
|
|
130
|
+
#
|
|
131
|
+
# class Comment
|
|
132
|
+
# include Him::Model
|
|
133
|
+
# end
|
|
134
|
+
#
|
|
135
|
+
# user = User.find(1)
|
|
136
|
+
# user.comments = [#<Comment(comments/2) id=2 body="Hello!">]
|
|
137
|
+
# user.comments.first.id = "Oops"
|
|
138
|
+
# user.comments.reload # => [#<Comment(comments/2) id=2 body="Hello!">]
|
|
139
|
+
# # Fetched again via GET "/users/1/comments"
|
|
140
|
+
def reload
|
|
141
|
+
reset
|
|
142
|
+
fetch
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
module Associations
|
|
4
|
+
class AssociationProxy < 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, :reload
|
|
19
|
+
|
|
20
|
+
install_proxy_methods 'association.fetch',
|
|
21
|
+
:class, :inspect, :==, :kind_of?, :is_a?, :respond_to?, :nil?, :hash, :eql?
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
def initialize(association)
|
|
25
|
+
@_her_association = association
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def association
|
|
29
|
+
@_her_association
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @private
|
|
33
|
+
def method_missing(name, *args, &block)
|
|
34
|
+
if name == :object_id # avoid redefining object_id
|
|
35
|
+
return association.fetch.object_id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# create a proxy to the fetched object's method
|
|
39
|
+
AssociationProxy.install_proxy_methods 'association.fetch', name
|
|
40
|
+
|
|
41
|
+
# resend message to fetched object
|
|
42
|
+
__send__(name, *args, &block)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Him
|
|
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
|
+
}.merge(opts)
|
|
15
|
+
klass.associations[:belongs_to] << 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, Him::Model::Associations::BelongsToAssociation.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
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# class User
|
|
36
|
+
# include Him::Model
|
|
37
|
+
# belongs_to :organization
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# class Organization
|
|
41
|
+
# include Him::Model
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# user = User.find(1)
|
|
45
|
+
# new_organization = user.organization.build(:name => "Foo Inc.")
|
|
46
|
+
# new_organization # => #<Organization name="Foo Inc.">
|
|
47
|
+
def build(attributes = {})
|
|
48
|
+
@klass.build(attributes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create a new object, save it and associate it to the parent
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# class User
|
|
55
|
+
# include Him::Model
|
|
56
|
+
# belongs_to :organization
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# class Organization
|
|
60
|
+
# include Him::Model
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# user = User.find(1)
|
|
64
|
+
# user.organization.create(:name => "Foo Inc.")
|
|
65
|
+
# user.organization # => #<Organization id=2 name="Foo Inc.">
|
|
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 fetch
|
|
74
|
+
foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
|
|
75
|
+
data_key_value = @parent.attributes[@opts[:data_key].to_sym]
|
|
76
|
+
return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (foreign_key_value.blank? && data_key_value.blank?)
|
|
77
|
+
|
|
78
|
+
return @cached_result unless @params.any? || @cached_result.nil?
|
|
79
|
+
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
|
80
|
+
|
|
81
|
+
path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
|
|
82
|
+
path = build_association_path -> { @klass.build_request_path(@opts[:path], path_params) }
|
|
83
|
+
@klass.get_resource(path, @params).tap do |result|
|
|
84
|
+
@cached_result = result if @params.blank?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @private
|
|
89
|
+
def assign_nested_attributes(attributes)
|
|
90
|
+
assign_single_nested_attributes(attributes)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Him
|
|
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 => Him::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, Him::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] => klass.instantiate_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 Him::Model
|
|
42
|
+
# has_many :comments
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# class Comment
|
|
46
|
+
# include Him::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
|
+
def build(attributes = {})
|
|
53
|
+
resource = @klass.build(attributes.merge(foreign_key => @parent.id))
|
|
54
|
+
|
|
55
|
+
collection = @cached_result || @parent.attributes[@name] || Him::Collection.new
|
|
56
|
+
collection << resource
|
|
57
|
+
@cached_result = collection
|
|
58
|
+
@parent.attributes[@name] = collection
|
|
59
|
+
|
|
60
|
+
resource
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Create a new object, save it and add it to the associated collection
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# class User
|
|
67
|
+
# include Him::Model
|
|
68
|
+
# has_many :comments
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# class Comment
|
|
72
|
+
# include Him::Model
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
# user = User.find(1)
|
|
76
|
+
# user.comments.create(:body => "Hello!")
|
|
77
|
+
# user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
|
|
78
|
+
def create(attributes = {})
|
|
79
|
+
resource = build(attributes)
|
|
80
|
+
|
|
81
|
+
unless resource.save
|
|
82
|
+
# Remove the built resource from the collection if save failed
|
|
83
|
+
@cached_result&.delete(resource)
|
|
84
|
+
@parent.attributes[@name]&.delete(resource)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
resource
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @private
|
|
91
|
+
def fetch
|
|
92
|
+
super.tap do |o|
|
|
93
|
+
inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
|
|
94
|
+
o.each do |entry|
|
|
95
|
+
inverse_association = entry.send(inverse_of).association rescue nil
|
|
96
|
+
if inverse_association.present?
|
|
97
|
+
inverse_association.cached_result = @parent
|
|
98
|
+
else
|
|
99
|
+
entry.attributes[inverse_of] = @parent
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @private
|
|
106
|
+
def assign_nested_attributes(attributes)
|
|
107
|
+
data = attributes.is_a?(Hash) ? attributes.values : attributes
|
|
108
|
+
@parent.attributes[@name] = @klass.instantiate_collection(@klass, :data => data)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Him
|
|
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, Him::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 Him::Model
|
|
37
|
+
# has_one :role
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# class Role
|
|
41
|
+
# include Him::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(foreign_key => @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 Him::Model
|
|
56
|
+
# has_one :role
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# class Role
|
|
60
|
+
# include Him::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
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require "him/model/associations/association"
|
|
2
|
+
require "him/model/associations/association_proxy"
|
|
3
|
+
require "him/model/associations/belongs_to_association"
|
|
4
|
+
require "him/model/associations/has_many_association"
|
|
5
|
+
require "him/model/associations/has_one_association"
|
|
6
|
+
|
|
7
|
+
module Him
|
|
8
|
+
module Model
|
|
9
|
+
# This module adds associations to models
|
|
10
|
+
module Associations
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
# Returns true if the model has a association_name association, false otherwise.
|
|
14
|
+
#
|
|
15
|
+
# @private
|
|
16
|
+
def has_association?(association_name)
|
|
17
|
+
associations = self.class.associations.values.flatten.map { |r| r[:name] }
|
|
18
|
+
associations.include?(association_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the resource/collection corresponding to the association_name association.
|
|
22
|
+
#
|
|
23
|
+
# @private
|
|
24
|
+
def get_association(association_name)
|
|
25
|
+
send(association_name) if has_association?(association_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
# Return @_her_associations, lazily initialized with copy of the
|
|
30
|
+
# superclass' her_associations, or an empty hash.
|
|
31
|
+
#
|
|
32
|
+
# @private
|
|
33
|
+
def associations
|
|
34
|
+
@_her_associations ||= begin
|
|
35
|
+
superclass.respond_to?(:associations) ? superclass.associations.dup : Hash.new { |h, k| h[k] = [] }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @private
|
|
40
|
+
def association_names
|
|
41
|
+
associations.inject([]) { |memo, (_, details)| memo << details }.flatten.map { |a| a[:name] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @private
|
|
45
|
+
def association_keys
|
|
46
|
+
associations.inject([]) { |memo, (_, details)| memo << details }.flatten.map { |a| a[:data_key] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse associations data after initializing a new object
|
|
50
|
+
#
|
|
51
|
+
# @private
|
|
52
|
+
def parse_associations(data)
|
|
53
|
+
associations.each_pair do |type, definitions|
|
|
54
|
+
definitions.each do |association|
|
|
55
|
+
association_class = "him/model/associations/#{type}_association".classify.constantize
|
|
56
|
+
data.merge! association_class.parse(association, self, data)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
data
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Define an *has_many* association.
|
|
64
|
+
#
|
|
65
|
+
# @param [Symbol] name The name of the method added to resources
|
|
66
|
+
# @param [Hash] opts Options
|
|
67
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
|
68
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
|
69
|
+
# @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# class User
|
|
73
|
+
# include Him::Model
|
|
74
|
+
# has_many :articles
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# class Article
|
|
78
|
+
# include Him::Model
|
|
79
|
+
# end
|
|
80
|
+
#
|
|
81
|
+
# @user = User.find(1)
|
|
82
|
+
# @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
|
|
83
|
+
# # Fetched via GET "/users/1/articles"
|
|
84
|
+
def has_many(name, opts = {})
|
|
85
|
+
Him::Model::Associations::HasManyAssociation.attach(self, name, opts)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Define an *has_one* association.
|
|
89
|
+
#
|
|
90
|
+
# @param [Symbol] name The name of the method added to resources
|
|
91
|
+
# @param [Hash] opts Options
|
|
92
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
|
93
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
|
94
|
+
# @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# class User
|
|
98
|
+
# include Him::Model
|
|
99
|
+
# has_one :organization
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
# class Organization
|
|
103
|
+
# include Him::Model
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# @user = User.find(1)
|
|
107
|
+
# @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
|
|
108
|
+
# # Fetched via GET "/users/1/organization"
|
|
109
|
+
def has_one(name, opts = {})
|
|
110
|
+
Him::Model::Associations::HasOneAssociation.attach(self, name, opts)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Define a *belongs_to* association.
|
|
114
|
+
#
|
|
115
|
+
# @param [Symbol] name The name of the method added to resources
|
|
116
|
+
# @param [Hash] opts Options
|
|
117
|
+
# @option opts [String] :class_name The name of the class to map objects to
|
|
118
|
+
# @option opts [Symbol] :data_key The attribute where the data is stored
|
|
119
|
+
# @option opts [Path] :path The relative path where to fetch the data
|
|
120
|
+
# @option opts [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# class User
|
|
124
|
+
# include Him::Model
|
|
125
|
+
# belongs_to :team, :class_name => "Group"
|
|
126
|
+
# end
|
|
127
|
+
#
|
|
128
|
+
# class Group
|
|
129
|
+
# include Him::Model
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
# @user = User.find(1) # => #<User(users/1) id=1 team_id=2 name="Tobias">
|
|
133
|
+
# @user.team # => #<Team(teams/2) id=2 name="Developers">
|
|
134
|
+
# # Fetched via GET "/teams/2"
|
|
135
|
+
def belongs_to(name, opts = {})
|
|
136
|
+
Him::Model::Associations::BelongsToAssociation.attach(self, name, opts)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|