active_remote 5.1.1 → 5.2.0.alpha

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d01c0d9e90a995d69eca7eb4fc5778033dc78b6c2a1a2ed1472f828833d1ffb
4
- data.tar.gz: 3304e91fb6c6dbf24955f5f84bccf1f19e4fb2f9a9a16a8a58c0dc41416f9e18
3
+ metadata.gz: 1d8212d6102d24ca632d2f76449826bf4593b2a76a0d27269866a0d3533b4813
4
+ data.tar.gz: e648845212e9756304427bccfc60da60d2aaf2325e309639905cfeb163e234bb
5
5
  SHA512:
6
- metadata.gz: 6ac0f959ee3b70f396917d360c8b1c28c663530dc0b4d1f8b890ce277e401663208a80da549917f204a7a6f628b60cdb8e278d348f884b33d87ea5bc17503e56
7
- data.tar.gz: d5db551ce3a30686ebe5bc529e2511a8e1b20b669d7ffd2b4b666f24d94bcbfec3b06cbda7787f03f469d5c87ffc7f7ce53b100f401795aea5f18cfa450d5850
6
+ metadata.gz: 89eed8d66c19ddc8daecd21db103ff91d29a5d2b779770163e7962a85a33c32c4c2d41f64ae8e27d950e87ddfa563350058a3478aa56e4b6ce50618405b1582f
7
+ data.tar.gz: d137fab5f02b7f753be9122f6aabe18c9593456a740384e1da0b80aca8ac1d9eaaaf6e8c8ca5c7d5cd2dae58c971ac04e0f5c274301d90a71f3ae1f3215502f9
@@ -20,8 +20,8 @@ Gem::Specification.new do |s|
20
20
  ##
21
21
  # Dependencies
22
22
  #
23
- s.add_dependency "activemodel", "~> 5.1.0"
24
- s.add_dependency "activesupport", "~> 5.1.0"
23
+ s.add_dependency "activemodel", "~> 5.2"
24
+ s.add_dependency "activesupport", "~> 5.2"
25
25
  s.add_dependency "protobuf", ">= 3.0"
26
26
 
27
27
  ##
@@ -5,7 +5,14 @@ require "bundler/setup"
5
5
  require "active_remote"
6
6
  require "benchmark/ips"
7
7
 
8
- require "./spec/support/models/typecasted_author"
8
+ class Author < ::ActiveRemote::Base
9
+ attribute :guid, :string
10
+ attribute :name, :string
11
+ attribute :age, :integer
12
+ attribute :birthday, :datetime
13
+ attribute :writes_fiction, :boolean
14
+ attribute :net_sales, :float
15
+ end
9
16
 
10
17
  ATTRIBUTES = {
11
18
  :guid => "0c030733-3b78-4587-b94b-5e0cf26497c5",
@@ -19,18 +26,18 @@ ATTRIBUTES = {
19
26
  ::Benchmark.ips do |x|
20
27
  x.config(:time => 20, :warmup => 10)
21
28
  x.report("initialize") do
22
- ::TypecastedAuthor.new(ATTRIBUTES)
29
+ ::Author.new(ATTRIBUTES)
23
30
  end
24
31
 
25
32
  x.report("instantiate") do
26
- ::TypecastedAuthor.instantiate(ATTRIBUTES)
33
+ ::Author.instantiate(ATTRIBUTES)
27
34
  end
28
35
 
29
36
  x.report("init attributes") do
30
- ::TypecastedAuthor.new(ATTRIBUTES).attributes
37
+ ::Author.new(ATTRIBUTES).attributes
31
38
  end
32
39
 
33
40
  x.report("inst attributes") do
34
- ::TypecastedAuthor.instantiate(ATTRIBUTES).attributes
41
+ ::Author.instantiate(ATTRIBUTES).attributes
35
42
  end
36
43
  end
@@ -8,8 +8,7 @@ module ActiveRemote
8
8
  # class must be loaded into memory already. A method will be defined
9
9
  # with the same name as the association. When invoked, the associated
10
10
  # remote model will issue a `search` for the :guid with the associated
11
- # guid's attribute (e.g. read_attribute(:client_guid)) and return the first
12
- # remote object from the result, or nil.
11
+ # guid attribute and return the first remote object from the result, or nil.
13
12
  #
14
13
  # A `belongs_to` association should be used when the associating remote
15
14
  # contains the guid to the associated model. For example, if a User model
@@ -37,8 +36,8 @@ module ActiveRemote
37
36
  perform_association(belongs_to_klass, options) do |klass, object|
38
37
  foreign_key = options.fetch(:foreign_key) { :"#{klass.name.demodulize.underscore}_guid" }
39
38
  search_hash = {}
40
- search_hash[:guid] = object.read_attribute(foreign_key)
41
- search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.key?(:scope)
39
+ search_hash[:guid] = object.send(foreign_key)
40
+ search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
42
41
 
43
42
  search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
44
43
  end
@@ -49,7 +48,7 @@ module ActiveRemote
49
48
  # class must be loaded into memory already. A method will be defined
50
49
  # with the same plural name as the association. When invoked, the associated
51
50
  # remote model will issue a `search` for the :guid with the associated
52
- # guid's attribute (e.g. read_attribute(:client_guid)).
51
+ # guid attribute.
53
52
  #
54
53
  # A `has_many` association should be used when the associated model has
55
54
  # a field to identify the associating model, and there can be multiple
@@ -79,7 +78,7 @@ module ActiveRemote
79
78
  foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
80
79
  search_hash = {}
81
80
  search_hash[foreign_key] = object.guid
82
- search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.key?(:scope)
81
+ search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
83
82
 
84
83
  search_hash.values.any?(&:nil?) ? [] : klass.search(search_hash)
85
84
  end
@@ -93,8 +92,7 @@ module ActiveRemote
93
92
  # class must be loaded into memory already. A method will be defined
94
93
  # with the same name as the association. When invoked, the associated
95
94
  # remote model will issue a `search` for the :guid with the associated
96
- # guid's attribute (e.g. read_attribute(:client_guid)) and return the first
97
- # remote object in the result, or nil.
95
+ # guid attribute and return the first remote object in the result, or nil.
98
96
  #
99
97
  # A `has_one` association should be used when the associated remote
100
98
  # contains the guid from the associating model. For example, if a User model
@@ -120,7 +118,7 @@ module ActiveRemote
120
118
  foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
121
119
  search_hash = {}
122
120
  search_hash[foreign_key] = object.guid
123
- search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.key?(:scope)
121
+ search_hash[options[:scope]] = object.send(options[:scope]) if options.key?(:scope)
124
122
 
125
123
  search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
126
124
  end
@@ -0,0 +1,51 @@
1
+ module ActiveRemote
2
+ module AttributeMethods
3
+ extend ::ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def attribute_names
7
+ @attribute_names ||= attribute_types.keys
8
+ end
9
+ end
10
+
11
+ def [](name)
12
+ attribute(name)
13
+ end
14
+
15
+ def []=(name, value)
16
+ write_attribute(name, value)
17
+ end
18
+
19
+ # Returns an <tt>#inspect</tt>-like string for the value of the
20
+ # attribute +attr_name+. String attributes are truncated up to 50
21
+ # characters, Date and Time attributes are returned in the
22
+ # <tt>:db</tt> format. Other attributes return the value of
23
+ # <tt>#inspect</tt> without modification.
24
+ #
25
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
26
+ #
27
+ # person.attribute_for_inspect(:name)
28
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
29
+ #
30
+ # person.attribute_for_inspect(:created_at)
31
+ # # => "\"2012-10-22 00:15:07\""
32
+ #
33
+ # person.attribute_for_inspect(:tag_ids)
34
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
35
+ def attribute_for_inspect(attr_name)
36
+ value = attribute(attr_name)
37
+
38
+ if value.is_a?(String) && value.length > 50
39
+ "#{value[0, 50]}...".inspect
40
+ elsif value.is_a?(Date) || value.is_a?(Time)
41
+ %("#{value.to_s(:db)}")
42
+ else
43
+ value.inspect
44
+ end
45
+ end
46
+
47
+ def attribute_names
48
+ @attributes.keys
49
+ end
50
+ end
51
+ end
@@ -1,8 +1,7 @@
1
1
  require "active_model/callbacks"
2
2
 
3
3
  require "active_remote/association"
4
- require "active_remote/attribute_definition"
5
- require "active_remote/attributes"
4
+ require "active_remote/attribute_methods"
6
5
  require "active_remote/config"
7
6
  require "active_remote/dirty"
8
7
  require "active_remote/dsl"
@@ -21,14 +20,16 @@ module ActiveRemote
21
20
  extend ::ActiveModel::Callbacks
22
21
 
23
22
  include ::ActiveModel::Model
23
+ include ::ActiveModel::Attributes
24
24
 
25
25
  include ::ActiveRemote::Association
26
- include ::ActiveRemote::Attributes
26
+ include ::ActiveRemote::AttributeMethods
27
27
  include ::ActiveRemote::DSL
28
28
  include ::ActiveRemote::Integration
29
+ include ::ActiveRemote::QueryAttributes
29
30
  include ::ActiveRemote::Persistence
30
31
  include ::ActiveRemote::PrimaryKey
31
- include ::ActiveRemote::QueryAttributes
32
+
32
33
  include ::ActiveRemote::RPC
33
34
  include ::ActiveRemote::ScopeKeys
34
35
  include ::ActiveRemote::Search
@@ -45,10 +46,7 @@ module ActiveRemote
45
46
  define_model_callbacks :initialize, :only => :after
46
47
 
47
48
  def initialize(attributes = {})
48
- @attributes = self.class.send(:default_attributes_hash).dup
49
-
50
- assign_attributes(attributes) if attributes
51
-
49
+ super
52
50
  @new_record = true
53
51
 
54
52
  skip_dirty_tracking do
@@ -58,6 +56,41 @@ module ActiveRemote
58
56
  end
59
57
  end
60
58
 
59
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
60
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
61
+ #
62
+ # Note that new records are different from any other record by definition, unless the
63
+ # other record is the receiver itself. Besides, if you fetch existing records with
64
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
65
+ #
66
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
67
+ # models are still comparable.
68
+ def ==(other)
69
+ super ||
70
+ other.instance_of?(self.class) &&
71
+ !send(primary_key).nil? &&
72
+ other.send(primary_key) == send(primary_key)
73
+ end
74
+ alias_method :eql?, :==
75
+
76
+ # Allows sort on objects
77
+ def <=>(other)
78
+ if other.is_a?(self.class)
79
+ to_key <=> other.to_key
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def freeze
86
+ @attributes.freeze
87
+ self
88
+ end
89
+
90
+ def frozen?
91
+ @attributes.frozen?
92
+ end
93
+
61
94
  # Initialize an object with the attributes hash directly
62
95
  # When used with allocate, bypasses initialize
63
96
  def init_with(attributes)
@@ -69,15 +102,32 @@ module ActiveRemote
69
102
  self
70
103
  end
71
104
 
72
- def freeze
73
- @attributes.freeze
74
- self
105
+ # Returns the contents of the record as a nicely formatted string.
106
+ def inspect
107
+ # We check defined?(@attributes) not to issue warnings if the object is
108
+ # allocated but not initialized.
109
+ inspection = if defined?(@attributes) && @attributes
110
+ attribute_names.collect do |name, _|
111
+ if attribute?(name)
112
+ "#{name}: #{attribute_for_inspect(name)}"
113
+ else
114
+ name
115
+ end
116
+ end.compact.join(", ")
117
+ else
118
+ "not initialized"
119
+ end
120
+
121
+ "#<#{self.class} #{inspection}>"
75
122
  end
76
123
 
77
- def frozen?
78
- @attributes.frozen?
124
+ # Returns a hash of the given methods with their names as keys and returned values as values.
125
+ def slice(*methods)
126
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
79
127
  end
80
128
  end
81
129
 
82
- ActiveSupport.run_load_hooks(:active_remote, Base)
130
+ ::ActiveModel::Type.register(:value, ::ActiveModel::Type::Value)
131
+
132
+ ::ActiveSupport.run_load_hooks(:active_remote, Base)
83
133
  end
@@ -24,8 +24,7 @@ module ActiveRemote
24
24
  #
25
25
  def reload(*)
26
26
  super.tap do
27
- @previously_changed.try(:clear)
28
- changed_attributes.clear
27
+ clear_changes_information
29
28
  end
30
29
  end
31
30
 
@@ -41,8 +40,7 @@ module ActiveRemote
41
40
  #
42
41
  def save(*)
43
42
  if (status = super)
44
- @previously_changed = changes
45
- changed_attributes.clear
43
+ changes_applied
46
44
  end
47
45
 
48
46
  status
@@ -52,8 +50,7 @@ module ActiveRemote
52
50
  #
53
51
  def save!(*)
54
52
  super.tap do
55
- @previously_changed = changes
56
- changed_attributes.clear
53
+ changes_applied
57
54
  end
58
55
  end
59
56
 
@@ -77,7 +74,7 @@ module ActiveRemote
77
74
  # ActiveModel::Dirty.
78
75
  #
79
76
  def attribute=(name, value)
80
- __send__("#{name}_will_change!") if _active_remote_track_changes? && value != self[name]
77
+ send("#{name}_will_change!") if _active_remote_track_changes? && value != self[name]
81
78
  super
82
79
  end
83
80
 
@@ -3,48 +3,134 @@ module ActiveRemote
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- unless singleton_methods.include?(:cache_timestamp_format)
7
- ##
8
- # :singleton-method:
9
- # Indicates the format used to generate the timestamp format in the cache key.
10
- # This is +:number+, by default.
11
- #
12
- def self.cache_timestamp_format
13
- :number
14
- end
15
- end
6
+ ##
7
+ # :singleton-method:
8
+ # Indicates the format used to generate the timestamp in the cache key, if
9
+ # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
10
+ #
11
+ # This is +:usec+, by default.
12
+ class_attribute :cache_timestamp_format, :instance_writer => false, :default => :usec
13
+
14
+ ##
15
+ # :singleton-method:
16
+ # Indicates whether to use a stable #cache_key method that is accompanied
17
+ # by a changing version in the #cache_version method.
18
+ #
19
+ # This is +false+, by default until Rails 6.0.
20
+ class_attribute :cache_versioning, :instance_writer => false, :default => false
16
21
  end
17
22
 
18
- ##
19
- # Returns a String, which can be used for constructing an URL to this
20
- # object. The default implementation returns this record's guid as a String,
21
- # or nil if this record's unsaved.
23
+ # Returns a +String+, which Action Pack uses for constructing a URL to this
24
+ # object. The default implementation returns this record's id as a +String+,
25
+ # or +nil+ if this record's unsaved.
26
+ #
27
+ # For example, suppose that you have a User model, and that you have a
28
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
29
+ # construct a path with the user object's 'id' in it:
30
+ #
31
+ # user = User.find_by(name: 'Phusion')
32
+ # user_path(user) # => "/users/1"
33
+ #
34
+ # You can override +to_param+ in your model to make +user_path+ construct
35
+ # a path using the user's name instead of the user's id:
22
36
  #
23
- # user = User.search(:name => 'Phusion')
24
- # user.to_param # => "GUID-1"
37
+ # class User < ActiveRecord::Base
38
+ # def to_param # overridden
39
+ # name
40
+ # end
41
+ # end
42
+ #
43
+ # user = User.find_by(name: 'Phusion')
44
+ # user_path(user) # => "/users/Phusion"
25
45
  #
26
46
  def to_param
27
- self[:guid]&.to_s
47
+ key = send(primary_key)
48
+ key&.to_s
28
49
  end
29
50
 
30
- ##
31
- # Returns a cache key that can be used to identify this record.
32
- #
33
- # ==== Examples
51
+ # Returns a stable cache key that can be used to identify this record.
34
52
  #
35
53
  # Product.new.cache_key # => "products/new"
36
- # Person.search(:guid => "derp-5").cache_key # => "people/derp-5-20071224150000" (include updated_at)
37
- # Product.search(:guid => "derp-5").cache_key # => "products/derp-5"
54
+ # Product.find(5).cache_key # => "products/5"
55
+ #
56
+ # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier,
57
+ # the cache key will also include a version.
58
+ #
59
+ # Product.cache_versioning = false
60
+ # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
38
61
  #
39
62
  def cache_key
40
63
  case
41
64
  when new_record? then
42
- "#{self.class.name.underscore}/new"
65
+ "#{model_name.cache_key}/new"
43
66
  when ::ActiveRemote.config.default_cache_key_updated_at? && (timestamp = self[:updated_at]) then
44
67
  timestamp = timestamp.utc.to_s(self.class.cache_timestamp_format)
45
- "#{self.class.name.underscore}/#{self.to_param}-#{timestamp}"
68
+ "#{model_name.cache_key}/#{send(primary_key)}-#{timestamp}"
69
+ else
70
+ "#{model_name.cache_key}/#{send(primary_key)}"
71
+ end
72
+ end
73
+
74
+ # Returns a cache key along with the version.
75
+ def cache_key_with_version
76
+ if (version = cache_version)
77
+ "#{cache_key}-#{version}"
46
78
  else
47
- "#{self.class.name.underscore}/#{self.to_param}"
79
+ cache_key
80
+ end
81
+ end
82
+
83
+ # Returns a cache version that can be used together with the cache key to form
84
+ # a recyclable caching scheme. By default, the #updated_at column is used for the
85
+ # cache_version, but this method can be overwritten to return something else.
86
+ #
87
+ # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
88
+ # +false+ (which it is by default until Rails 6.0).
89
+ def cache_version
90
+ if cache_versioning && (timestamp = try(:updated_at))
91
+ timestamp.utc.to_s(:usec)
92
+ end
93
+ end
94
+
95
+ module ClassMethods
96
+ # Defines your model's +to_param+ method to generate "pretty" URLs
97
+ # using +method_name+, which can be any attribute or method that
98
+ # responds to +to_s+.
99
+ #
100
+ # class User < ActiveRecord::Base
101
+ # to_param :name
102
+ # end
103
+ #
104
+ # user = User.find_by(name: 'Fancy Pants')
105
+ # user.id # => 123
106
+ # user_path(user) # => "/users/123-fancy-pants"
107
+ #
108
+ # Values longer than 20 characters will be truncated. The value
109
+ # is truncated word by word.
110
+ #
111
+ # user = User.find_by(name: 'David Heinemeier Hansson')
112
+ # user.id # => 125
113
+ # user_path(user) # => "/users/125-david-heinemeier"
114
+ #
115
+ # Because the generated param begins with the record's +id+, it is
116
+ # suitable for passing to +find+. In a controller, for example:
117
+ #
118
+ # params[:id] # => "123-fancy-pants"
119
+ # User.find(params[:id]).id # => 123
120
+ def to_param(method_name = nil)
121
+ if method_name.nil?
122
+ super()
123
+ else
124
+ define_method :to_param do
125
+ if (default = super()) &&
126
+ (result = send(method_name).to_s).present? &&
127
+ (param = result.squish.parameterize.truncate(20, :separator => /-/, :omission => "")).present?
128
+ "#{default}-#{param}"
129
+ else
130
+ default
131
+ end
132
+ end
133
+ end
48
134
  end
49
135
  end
50
136
  end