active_remote 5.1.1 → 5.2.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
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