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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +232 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +55 -0
  8. data/.yardopts +2 -0
  9. data/CONTRIBUTING.md +26 -0
  10. data/Gemfile +10 -0
  11. data/HER_README.md +1065 -0
  12. data/LICENSE +7 -0
  13. data/README.md +7 -0
  14. data/Rakefile +11 -0
  15. data/UPGRADE.md +101 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +6 -0
  17. data/gemfiles/Gemfile.activemodel-5.0 +6 -0
  18. data/gemfiles/Gemfile.activemodel-5.1 +6 -0
  19. data/gemfiles/Gemfile.activemodel-5.2 +6 -0
  20. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  21. data/lib/restorm/api.rb +121 -0
  22. data/lib/restorm/collection.rb +13 -0
  23. data/lib/restorm/errors.rb +29 -0
  24. data/lib/restorm/json_api/model.rb +42 -0
  25. data/lib/restorm/middleware/accept_json.rb +18 -0
  26. data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
  27. data/lib/restorm/middleware/json_api_parser.rb +37 -0
  28. data/lib/restorm/middleware/parse_json.rb +22 -0
  29. data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
  30. data/lib/restorm/middleware.rb +12 -0
  31. data/lib/restorm/model/associations/association.rb +128 -0
  32. data/lib/restorm/model/associations/association_proxy.rb +44 -0
  33. data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
  34. data/lib/restorm/model/associations/has_many_association.rb +100 -0
  35. data/lib/restorm/model/associations/has_one_association.rb +79 -0
  36. data/lib/restorm/model/associations.rb +141 -0
  37. data/lib/restorm/model/attributes.rb +322 -0
  38. data/lib/restorm/model/base.rb +33 -0
  39. data/lib/restorm/model/deprecated_methods.rb +61 -0
  40. data/lib/restorm/model/http.rb +119 -0
  41. data/lib/restorm/model/introspection.rb +67 -0
  42. data/lib/restorm/model/nested_attributes.rb +45 -0
  43. data/lib/restorm/model/orm.rb +299 -0
  44. data/lib/restorm/model/parse.rb +223 -0
  45. data/lib/restorm/model/paths.rb +125 -0
  46. data/lib/restorm/model/relation.rb +209 -0
  47. data/lib/restorm/model.rb +75 -0
  48. data/lib/restorm/version.rb +3 -0
  49. data/lib/restorm.rb +19 -0
  50. data/restorm.gemspec +29 -0
  51. data/spec/api_spec.rb +120 -0
  52. data/spec/collection_spec.rb +41 -0
  53. data/spec/json_api/model_spec.rb +169 -0
  54. data/spec/middleware/accept_json_spec.rb +11 -0
  55. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  56. data/spec/middleware/json_api_parser_spec.rb +52 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/spec/model/associations/association_proxy_spec.rb +29 -0
  59. data/spec/model/associations_spec.rb +911 -0
  60. data/spec/model/attributes_spec.rb +354 -0
  61. data/spec/model/callbacks_spec.rb +176 -0
  62. data/spec/model/dirty_spec.rb +133 -0
  63. data/spec/model/http_spec.rb +201 -0
  64. data/spec/model/introspection_spec.rb +81 -0
  65. data/spec/model/nested_attributes_spec.rb +135 -0
  66. data/spec/model/orm_spec.rb +704 -0
  67. data/spec/model/parse_spec.rb +520 -0
  68. data/spec/model/paths_spec.rb +348 -0
  69. data/spec/model/relation_spec.rb +247 -0
  70. data/spec/model/validations_spec.rb +43 -0
  71. data/spec/model_spec.rb +45 -0
  72. data/spec/spec_helper.rb +25 -0
  73. data/spec/support/macros/her_macros.rb +17 -0
  74. data/spec/support/macros/model_macros.rb +36 -0
  75. data/spec/support/macros/request_macros.rb +27 -0
  76. 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