acfs 1.3.3 → 1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +339 -0
- data/LICENSE +22 -0
- data/README.md +335 -0
- data/acfs.gemspec +46 -0
- data/lib/acfs.rb +51 -0
- data/lib/acfs/adapter/base.rb +24 -0
- data/lib/acfs/adapter/typhoeus.rb +69 -0
- data/lib/acfs/collection.rb +28 -0
- data/lib/acfs/collections/paginatable.rb +76 -0
- data/lib/acfs/configuration.rb +120 -0
- data/lib/acfs/errors.rb +127 -0
- data/lib/acfs/global.rb +101 -0
- data/lib/acfs/location.rb +82 -0
- data/lib/acfs/middleware/base.rb +24 -0
- data/lib/acfs/middleware/json.rb +29 -0
- data/lib/acfs/middleware/logger.rb +25 -0
- data/lib/acfs/middleware/msgpack.rb +32 -0
- data/lib/acfs/middleware/print.rb +23 -0
- data/lib/acfs/middleware/serializer.rb +41 -0
- data/lib/acfs/operation.rb +83 -0
- data/lib/acfs/request.rb +39 -0
- data/lib/acfs/request/callbacks.rb +54 -0
- data/lib/acfs/resource.rb +39 -0
- data/lib/acfs/resource/attributes.rb +269 -0
- data/lib/acfs/resource/attributes/base.rb +29 -0
- data/lib/acfs/resource/attributes/boolean.rb +39 -0
- data/lib/acfs/resource/attributes/date_time.rb +32 -0
- data/lib/acfs/resource/attributes/dict.rb +39 -0
- data/lib/acfs/resource/attributes/float.rb +33 -0
- data/lib/acfs/resource/attributes/integer.rb +29 -0
- data/lib/acfs/resource/attributes/list.rb +36 -0
- data/lib/acfs/resource/attributes/string.rb +26 -0
- data/lib/acfs/resource/attributes/uuid.rb +48 -0
- data/lib/acfs/resource/dirty.rb +37 -0
- data/lib/acfs/resource/initialization.rb +31 -0
- data/lib/acfs/resource/loadable.rb +35 -0
- data/lib/acfs/resource/locatable.rb +132 -0
- data/lib/acfs/resource/operational.rb +23 -0
- data/lib/acfs/resource/persistence.rb +260 -0
- data/lib/acfs/resource/query_methods.rb +266 -0
- data/lib/acfs/resource/service.rb +44 -0
- data/lib/acfs/resource/validation.rb +39 -0
- data/lib/acfs/response.rb +30 -0
- data/lib/acfs/response/formats.rb +27 -0
- data/lib/acfs/response/status.rb +33 -0
- data/lib/acfs/rspec.rb +13 -0
- data/lib/acfs/runner.rb +102 -0
- data/lib/acfs/service.rb +97 -0
- data/lib/acfs/service/middleware.rb +58 -0
- data/lib/acfs/service/middleware/stack.rb +65 -0
- data/lib/acfs/singleton_resource.rb +85 -0
- data/lib/acfs/stub.rb +194 -0
- data/lib/acfs/util.rb +22 -0
- data/lib/acfs/version.rb +16 -0
- data/lib/acfs/yard.rb +6 -0
- data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
- data/spec/acfs/collection_spec.rb +157 -0
- data/spec/acfs/configuration_spec.rb +53 -0
- data/spec/acfs/global_spec.rb +140 -0
- data/spec/acfs/location_spec.rb +25 -0
- data/spec/acfs/middleware/json_spec.rb +65 -0
- data/spec/acfs/middleware/msgpack_spec.rb +62 -0
- data/spec/acfs/operation_spec.rb +12 -0
- data/spec/acfs/request/callbacks_spec.rb +48 -0
- data/spec/acfs/request_spec.rb +79 -0
- data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
- data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
- data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
- data/spec/acfs/resource/attributes/float_spec.rb +61 -0
- data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
- data/spec/acfs/resource/attributes/list_spec.rb +60 -0
- data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
- data/spec/acfs/resource/attributes_spec.rb +181 -0
- data/spec/acfs/resource/dirty_spec.rb +49 -0
- data/spec/acfs/resource/initialization_spec.rb +36 -0
- data/spec/acfs/resource/loadable_spec.rb +22 -0
- data/spec/acfs/resource/locatable_spec.rb +118 -0
- data/spec/acfs/resource/persistance_spec.rb +322 -0
- data/spec/acfs/resource/query_methods_spec.rb +548 -0
- data/spec/acfs/resource/validation_spec.rb +129 -0
- data/spec/acfs/response/formats_spec.rb +52 -0
- data/spec/acfs/response/status_spec.rb +71 -0
- data/spec/acfs/runner_spec.rb +95 -0
- data/spec/acfs/service/middleware_spec.rb +35 -0
- data/spec/acfs/service_spec.rb +48 -0
- data/spec/acfs/singleton_resource_spec.rb +17 -0
- data/spec/acfs/stub_spec.rb +345 -0
- data/spec/acfs_spec.rb +205 -0
- data/spec/fixtures/config.yml +14 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/hash.rb +11 -0
- data/spec/support/response.rb +12 -0
- data/spec/support/service.rb +92 -0
- data/spec/support/shared/find_callbacks.rb +50 -0
- metadata +136 -3
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# DateTime attribute type. Use it in your model as
|
7
|
+
# an attribute type:
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class User < Acfs::Resource
|
11
|
+
# attribute :name, :date_time
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
class DateTime < Base
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# Cast given object to DateTime.
|
18
|
+
#
|
19
|
+
# @param [Object] value Object to cast.
|
20
|
+
# @return [DateTime] Casted object as DateTime.
|
21
|
+
#
|
22
|
+
def cast_value(value)
|
23
|
+
if value.blank?
|
24
|
+
nil
|
25
|
+
elsif value.acts_like?(:time) || value.acts_like?(:date)
|
26
|
+
value.to_datetime
|
27
|
+
else
|
28
|
+
::DateTime.iso8601 value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# Dict attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User
|
10
|
+
# include Acfs::Model
|
11
|
+
# attribute :opts, :dict
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
class Dict < Base
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# Cast given object to a dict/hash.
|
18
|
+
#
|
19
|
+
# @param [Object] value Object to cast.
|
20
|
+
# @return [Hash] Casted object as hash.
|
21
|
+
# @raise [TypeError] If object cannot be casted to a hash.
|
22
|
+
#
|
23
|
+
def cast_value(value)
|
24
|
+
return {} if value.blank?
|
25
|
+
|
26
|
+
if value.is_a?(Hash)
|
27
|
+
value
|
28
|
+
elsif value.respond_to?(:serializable_hash)
|
29
|
+
value.serializable_hash
|
30
|
+
elsif value.respond_to?(:to_hash)
|
31
|
+
value.to_hash
|
32
|
+
elsif value.respond_to?(:to_h)
|
33
|
+
value.to_h
|
34
|
+
else
|
35
|
+
Hash(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# Float attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User < Acfs::Resource
|
10
|
+
# attribute :name, :float
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Float < Base
|
14
|
+
# @api public
|
15
|
+
#
|
16
|
+
# Cast given object to float.
|
17
|
+
#
|
18
|
+
# @param [Object] value Object to cast.
|
19
|
+
# @return [Float] Casted object as float.
|
20
|
+
#
|
21
|
+
def cast_value(value)
|
22
|
+
return 0.0 if value.blank?
|
23
|
+
|
24
|
+
case value
|
25
|
+
when ::Float then value
|
26
|
+
when 'Infinity' then ::Float::INFINITY
|
27
|
+
when '-Infinity' then -::Float::INFINITY
|
28
|
+
when 'NaN' then ::Float::NAN
|
29
|
+
else Float(value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# Integer attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User < Acfs::Resource
|
10
|
+
# attribute :name, :integer
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Integer < Base
|
14
|
+
# @api public
|
15
|
+
#
|
16
|
+
# Cast given object to integer.
|
17
|
+
#
|
18
|
+
# @param [Object] value Object to cast.
|
19
|
+
# @return [Fixnum] Casted object as fixnum.
|
20
|
+
#
|
21
|
+
def cast_value(value)
|
22
|
+
if value.blank?
|
23
|
+
0
|
24
|
+
else
|
25
|
+
Integer(value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# List attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User < Acfs::Resource
|
10
|
+
# attribute :name, :list
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class List < Base
|
14
|
+
# @api public
|
15
|
+
#
|
16
|
+
# Cast given object to a list.
|
17
|
+
#
|
18
|
+
# @param [Object] value Object to cast.
|
19
|
+
# @return [Fixnum] Casted object as list.
|
20
|
+
# @raise [TypeError] If object cannot be casted to a list.
|
21
|
+
#
|
22
|
+
def cast_value(value)
|
23
|
+
return [] if value.blank?
|
24
|
+
|
25
|
+
if value.is_a?(::Array)
|
26
|
+
value
|
27
|
+
elsif value.respond_to?(:to_ary)
|
28
|
+
value.to_ary
|
29
|
+
elsif value.respond_to?(:to_a)
|
30
|
+
value.to_a
|
31
|
+
else
|
32
|
+
Array(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# String attribute type. Use it in your model as
|
7
|
+
# an attribute type:
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class User < Acfs::Resource
|
11
|
+
# attribute :name, :string
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
class String < Base
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# Cast given object to string.
|
18
|
+
#
|
19
|
+
# @param [Object] value Object to cast.
|
20
|
+
# @return [String] Casted string.
|
21
|
+
#
|
22
|
+
def cast_value(value)
|
23
|
+
value.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# UUID attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User < Acfs::Resource
|
10
|
+
# attribute :id, :uuid
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class UUID < Base
|
14
|
+
UUID_REGEXP = /[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/i.freeze
|
15
|
+
|
16
|
+
# @api public
|
17
|
+
#
|
18
|
+
# Check if given object looks like a UUID, eg:
|
19
|
+
# `450b7a40-94ad-11e3-baa8-0800200c9a66`
|
20
|
+
# Valid UUIDs are 16 byte numbers represented as
|
21
|
+
# a hexadecimal string in five sub-groups seperated
|
22
|
+
# by a dash. Each group has to consist of a fixed
|
23
|
+
# number of hexadecimal digits:
|
24
|
+
# | Group | Digits |
|
25
|
+
# | -----:|:------ |
|
26
|
+
# | 1 | 8 |
|
27
|
+
# | 2 | 4 |
|
28
|
+
# | 3 | 4 |
|
29
|
+
# | 4 | 4 |
|
30
|
+
# | 5 | 12 |
|
31
|
+
#
|
32
|
+
# @param [Object] value Object to cast.
|
33
|
+
# @return [String] Casted object as UUID.
|
34
|
+
#
|
35
|
+
def cast_value(value)
|
36
|
+
if value.blank?
|
37
|
+
nil
|
38
|
+
elsif value.to_s =~ UUID_REGEXP
|
39
|
+
value
|
40
|
+
else
|
41
|
+
raise TypeError.new "Invalid UUID: `#{value}'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lower-case alias for automatic type lookup
|
47
|
+
Uuid = UUID
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
#
|
5
|
+
# Thin wrapper around ActiveModel::Dirty
|
6
|
+
#
|
7
|
+
module Dirty
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
include ActiveModel::Dirty
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
def reset_changes
|
14
|
+
clear_changes_information
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
#
|
19
|
+
def save!(*)
|
20
|
+
super.tap {|_| changes_applied }
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
#
|
25
|
+
def loaded!
|
26
|
+
reset_changes
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
def write_raw_attribute(name, value, opts = {})
|
33
|
+
attribute_will_change!(name) if opts[:change].nil? || opts[:change]
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
#
|
5
|
+
# Initialization drop-in for pre-4.0 ActiveModel.
|
6
|
+
#
|
7
|
+
module Initialization
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
#
|
11
|
+
# Initializes a new model with the given `params`. fff
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class User < Acfs::Resource
|
15
|
+
# attribute :name
|
16
|
+
# attribute :email, default: ->{ "#{name}@dom.tld" }
|
17
|
+
# attribute :age, :integer, default: 18
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# user = User.new(name: 'bob')
|
21
|
+
# user.name # => "bob"
|
22
|
+
# user.email # => "bob@dom.tld"
|
23
|
+
# user.age # => 18
|
24
|
+
#
|
25
|
+
# @param params [Hash{Symbol => Object}] Attributes to set on resource.
|
26
|
+
#
|
27
|
+
def initialize(params = {})
|
28
|
+
write_attributes params if params
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
# Provides method to check for loading state of resources.
|
5
|
+
# A resource that is created but not yet fetched will be loaded
|
6
|
+
# after running {Acfs::Global#run Acfs.run}.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# user = User.find 5
|
10
|
+
# user.loaded? # => false
|
11
|
+
# Acfs.run
|
12
|
+
# user.loaded? # => true
|
13
|
+
#
|
14
|
+
module Loadable
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
# @api public
|
18
|
+
#
|
19
|
+
# Check if model is loaded or if request is still queued.
|
20
|
+
#
|
21
|
+
# @return [Boolean] True if resource is loaded, false otherwise.
|
22
|
+
#
|
23
|
+
def loaded?
|
24
|
+
@loaded.nil? ? false : @loaded
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
#
|
29
|
+
# Mark model as loaded.
|
30
|
+
#
|
31
|
+
def loaded!
|
32
|
+
@loaded = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
# Provide methods for generation URLs for resources.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class User
|
8
|
+
# service AccountService # With base URL `http://acc.svr`
|
9
|
+
# end
|
10
|
+
# User.url # => "http://acc.svr/users"
|
11
|
+
# User.url(5) # => "http://acc.svr/users/5"
|
12
|
+
#
|
13
|
+
module Locatable
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# @overload url(suffix)
|
18
|
+
# @deprecated
|
19
|
+
# Return URL for this class of resource. Given suffix
|
20
|
+
# will be appended.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# User.url # => "http://users.srv.org/users"
|
24
|
+
# User.url(5) # => "http://users.srv.org/users/5"
|
25
|
+
#
|
26
|
+
# @param suffix [String] Suffix to append to URL.
|
27
|
+
# @return [String] Generated URL.
|
28
|
+
#
|
29
|
+
# @overload url(opts = {})
|
30
|
+
# Return URL for this class of resources. Given options
|
31
|
+
# will be used to replace URL path arguments and to
|
32
|
+
# determine the operation action.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# User.url(id: 5, action: :read) # => "http://users.srv.org/users/5"
|
36
|
+
# User.url(action: :list) # => "http://users.srv.org/users"
|
37
|
+
#
|
38
|
+
# @param opts [Hash] Options.
|
39
|
+
# @option opts [Symbol] :action Operation action,
|
40
|
+
# usually `:list`, `:create`, `:read`, `:update` or`:delete`.
|
41
|
+
# @return [String] Generated URL.
|
42
|
+
#
|
43
|
+
def url(suffix = nil, opts = {})
|
44
|
+
if suffix.is_a? Hash
|
45
|
+
opts = suffix
|
46
|
+
suffix = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
opts[:action] = :list if suffix
|
50
|
+
|
51
|
+
url = location(opts).build(opts).str
|
52
|
+
url += "/#{suffix}" if suffix.to_s.present?
|
53
|
+
url
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return a location object able to build the URL for this
|
57
|
+
# resource and given action.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# class Identity < ::Acfs::Resource
|
61
|
+
# service MyService, path: 'users/:user_id/identities'
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# location = Identity.location(action: :read)
|
65
|
+
# location.arguments
|
66
|
+
# => [:user_id, :id]
|
67
|
+
#
|
68
|
+
# location.raw_url
|
69
|
+
# => 'http://service/users/:user_id/identities/:id'
|
70
|
+
#
|
71
|
+
# location = Identity.location(action: :list)
|
72
|
+
# location.arguments
|
73
|
+
# => [:user_id]
|
74
|
+
#
|
75
|
+
# location.build(user_id: 42)
|
76
|
+
# => 'http://service/users/42/identities'
|
77
|
+
#
|
78
|
+
# @param opts [Hash] Options.
|
79
|
+
# @option opts [Symbol] :action Operation action,
|
80
|
+
# usually `:list`, `:create`, `:read`, `:update` or`:delete`.
|
81
|
+
#
|
82
|
+
# @return [Location] Location object.
|
83
|
+
#
|
84
|
+
def location(opts = {})
|
85
|
+
service.location(self, opts)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
def location_default_path(action, path)
|
90
|
+
case action
|
91
|
+
when :list, :create
|
92
|
+
path
|
93
|
+
when :read, :update, :delete
|
94
|
+
"#{path}/:id"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return URL for this resource. Resource if will be appended
|
100
|
+
# as suffix if present.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# user.new.url # => "http://users.srv.org/users"
|
104
|
+
#
|
105
|
+
# user = User.find 5
|
106
|
+
# Acfs.run
|
107
|
+
# user.url # => "http://users.srv.org/users/5"
|
108
|
+
#
|
109
|
+
# @return [ String ] Generated URL.
|
110
|
+
# @see ClassMethods#url
|
111
|
+
#
|
112
|
+
def url(opts = {})
|
113
|
+
return nil if need_primary_key? && !primary_key?
|
114
|
+
|
115
|
+
self.class.service
|
116
|
+
.location(self.class, opts.reverse_merge(action: :read))
|
117
|
+
.build(attributes).str
|
118
|
+
end
|
119
|
+
|
120
|
+
# @api private
|
121
|
+
# Return true if resource needs a primary key (id) for singular actions.
|
122
|
+
def need_primary_key?
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
# @api private
|
127
|
+
# Return true if resource has a primary key (id) set.
|
128
|
+
def primary_key?
|
129
|
+
respond_to?(:id) && !id.nil?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|