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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. 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