restorm 1.0.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/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +232 -0
- data/.ruby-version +1 -0
- data/.travis.yml +55 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/HER_README.md +1065 -0
- data/LICENSE +7 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-4.2 +6 -0
- data/gemfiles/Gemfile.activemodel-5.0 +6 -0
- data/gemfiles/Gemfile.activemodel-5.1 +6 -0
- data/gemfiles/Gemfile.activemodel-5.2 +6 -0
- data/gemfiles/Gemfile.faraday-1.0 +6 -0
- data/lib/restorm/api.rb +121 -0
- data/lib/restorm/collection.rb +13 -0
- data/lib/restorm/errors.rb +29 -0
- data/lib/restorm/json_api/model.rb +42 -0
- data/lib/restorm/middleware/accept_json.rb +18 -0
- data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
- data/lib/restorm/middleware/json_api_parser.rb +37 -0
- data/lib/restorm/middleware/parse_json.rb +22 -0
- data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
- data/lib/restorm/middleware.rb +12 -0
- data/lib/restorm/model/associations/association.rb +128 -0
- data/lib/restorm/model/associations/association_proxy.rb +44 -0
- data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
- data/lib/restorm/model/associations/has_many_association.rb +100 -0
- data/lib/restorm/model/associations/has_one_association.rb +79 -0
- data/lib/restorm/model/associations.rb +141 -0
- data/lib/restorm/model/attributes.rb +322 -0
- data/lib/restorm/model/base.rb +33 -0
- data/lib/restorm/model/deprecated_methods.rb +61 -0
- data/lib/restorm/model/http.rb +119 -0
- data/lib/restorm/model/introspection.rb +67 -0
- data/lib/restorm/model/nested_attributes.rb +45 -0
- data/lib/restorm/model/orm.rb +299 -0
- data/lib/restorm/model/parse.rb +223 -0
- data/lib/restorm/model/paths.rb +125 -0
- data/lib/restorm/model/relation.rb +209 -0
- data/lib/restorm/model.rb +75 -0
- data/lib/restorm/version.rb +3 -0
- data/lib/restorm.rb +19 -0
- data/restorm.gemspec +29 -0
- data/spec/api_spec.rb +120 -0
- data/spec/collection_spec.rb +41 -0
- data/spec/json_api/model_spec.rb +169 -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 +911 -0
- data/spec/model/attributes_spec.rb +354 -0
- data/spec/model/callbacks_spec.rb +176 -0
- data/spec/model/dirty_spec.rb +133 -0
- data/spec/model/http_spec.rb +201 -0
- data/spec/model/introspection_spec.rb +81 -0
- data/spec/model/nested_attributes_spec.rb +135 -0
- data/spec/model/orm_spec.rb +704 -0
- data/spec/model/parse_spec.rb +520 -0
- data/spec/model/paths_spec.rb +348 -0
- data/spec/model/relation_spec.rb +247 -0
- data/spec/model/validations_spec.rb +43 -0
- data/spec/model_spec.rb +45 -0
- data/spec/spec_helper.rb +25 -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 +203 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module Restorm
|
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, :reload
|
19
|
+
|
20
|
+
# @private
|
21
|
+
def initialize(association)
|
22
|
+
@_restorm_association = association
|
23
|
+
end
|
24
|
+
|
25
|
+
def association
|
26
|
+
@_restorm_association
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def method_missing(name, *args, &block)
|
31
|
+
if name == :object_id # 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
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Restorm
|
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 = :"@_restorm_association_#{name}"
|
20
|
+
|
21
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
22
|
+
cached_data || instance_variable_set(cached_name, Restorm::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 Restorm::Model
|
37
|
+
# belongs_to :organization
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# class Organization
|
41
|
+
# include Restorm::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 Restorm::Model
|
56
|
+
# belongs_to :organization
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class Organization
|
60
|
+
# include Restorm::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,100 @@
|
|
1
|
+
module Restorm
|
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 => Restorm::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 = :"@_restorm_association_#{name}"
|
21
|
+
|
22
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
23
|
+
cached_data || instance_variable_set(cached_name, Restorm::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.restorm_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 Restorm::Model
|
42
|
+
# has_many :comments
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class Comment
|
46
|
+
# include Restorm::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 Restorm::Model
|
63
|
+
# has_many :comments
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# class Comment
|
67
|
+
# include Restorm::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] ||= Restorm::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.attributes[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] = @klass.instantiate_collection(@klass, :data => data)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Restorm
|
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 = :"@_restorm_association_#{name}"
|
20
|
+
|
21
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
22
|
+
cached_data || instance_variable_set(cached_name, Restorm::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 Restorm::Model
|
37
|
+
# has_one :role
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# class Role
|
41
|
+
# include Restorm::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 Restorm::Model
|
56
|
+
# has_one :role
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class Role
|
60
|
+
# include Restorm::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 "restorm/model/associations/association"
|
2
|
+
require "restorm/model/associations/association_proxy"
|
3
|
+
require "restorm/model/associations/belongs_to_association"
|
4
|
+
require "restorm/model/associations/has_many_association"
|
5
|
+
require "restorm/model/associations/has_one_association"
|
6
|
+
|
7
|
+
module Restorm
|
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 @_restorm_associations, lazily initialized with copy of the
|
30
|
+
# superclass' her_associations, or an empty hash.
|
31
|
+
#
|
32
|
+
# @private
|
33
|
+
def associations
|
34
|
+
@_restorm_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 = "restorm/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 Restorm::Model
|
74
|
+
# has_many :articles
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# class Article
|
78
|
+
# include Restorm::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
|
+
Restorm::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 Restorm::Model
|
99
|
+
# has_one :organization
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# class Organization
|
103
|
+
# include Restorm::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
|
+
Restorm::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 Restorm::Model
|
125
|
+
# belongs_to :team, :class_name => "Group"
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# class Group
|
129
|
+
# include Restorm::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
|
+
Restorm::Model::Associations::BelongsToAssociation.attach(self, name, opts)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|