cistern 0.3.2 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6c39b9ac9a702497cd0fd82279e908697d2d8e65
4
+ data.tar.gz: 7b9aae264e94c4dd5c5d883a697cf038659be657
5
+ SHA512:
6
+ metadata.gz: 8441ed13d9b55f0f1cf988ec91a47914174388bed261811fa9c4a5d65cea727c8252a65fc1af22b75187f5c044fa1a058c109ed9693efb4ef1606fbfa5039ad1
7
+ data.tar.gz: 258a085c417051866346609af569a7269d173d8652fae7906bb365884c4d60fb493a9d2d5f9feaab336566a5efc7f9dcc8c2749a5644be336381c28b858932b6
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.1.1"
4
+ - "1.9.3"
5
+ - "jruby-19mode"
6
+ bundler_args: "--without development"
7
+ before_install:
8
+ - "gem install bundler -v 1.5.2"
9
+ script: "bundle exec rake --trace"
10
+ notifications:
11
+ email:
12
+ on_success: never
13
+ on_failure: change
data/Gemfile CHANGED
@@ -1,18 +1,17 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in cistern.gemspec
3
+ # Specify your gem"s dependencies in cistern.gemspec
4
4
  gemspec
5
5
 
6
6
  group :test do
7
- gem "rspec", "~> 2.0"
8
7
  gem "guard-rspec"
8
+ gem "pry-nav"
9
9
  gem "rake"
10
- gem 'rb-fsevent', '~> 0.9.1'
11
- gem 'pry-nav'
10
+ gem "rspec", "~> 2.0"
12
11
  end
13
12
 
14
13
  group :formatters do
15
- gem 'formatador'
16
- gem 'awesome_print'
14
+ gem "formatador"
15
+ gem "awesome_print"
17
16
  end
18
17
 
data/Guardfile CHANGED
@@ -1,4 +1,4 @@
1
- guard 'rspec' do
1
+ guard 'rspec', cmd: 'bundle exec rspec' do
2
2
  watch(%r{^spec/.+_spec\.rb$})
3
3
  watch(%r{^lib/(.+)\.rb$}) { "spec" }
4
4
  watch('spec/spec_helper.rb') { "spec" }
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Josh Lane
1
+ Copyright (c) 2014 Josh Lane
2
2
 
3
3
  MIT License
4
4
 
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -90,8 +90,11 @@ class Foo::Client::Bar < Cistern::Model
90
90
  }
91
91
 
92
92
  if new_record?
93
- request_attributes = connection.create_bar(params).body["request"]
94
- merge_attributes(new_attributes)
93
+ merge_attributes(connection.create_bar(params).body["bar"])
94
+ else
95
+ requires :identity
96
+
97
+ merge_attributes(connection.update_bar(params).body["bar"])
95
98
  end
96
99
  end
97
100
  end
@@ -111,10 +114,10 @@ class Foo::Client::Bars < Cistern::Collection
111
114
  def all(params = {})
112
115
  response = connection.get_bars(params)
113
116
 
114
- data = self.clone.load(response.body["bars"])
117
+ data = response.body
115
118
 
116
- collection.attributes.clear
117
- collection.merge_attributes(data)
119
+ self.load(data["bars"]) # store bar records in collection
120
+ self.merge_attributes(data) # store any other attributes of the response on the collection
118
121
  end
119
122
 
120
123
  def discover(provisioned_id, options={})
data/cistern.gemspec CHANGED
@@ -6,6 +6,7 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["me@joshualane.com"]
7
7
  gem.description = %q{API client framework extracted from Fog}
8
8
  gem.summary = %q{API client framework}
9
+ gem.license = "MIT"
9
10
  gem.homepage = "http://joshualane.com/cistern"
10
11
 
11
12
  gem.files = `git ls-files`.split($\)
@@ -1,12 +1,12 @@
1
1
  module Cistern::Attributes
2
2
  def self.parsers
3
3
  @parsers ||= {
4
- :string => lambda{|v,opts| v.to_s},
5
- :time => lambda{|v,opts| v.is_a?(Time) ? v : v && Time.parse(v.to_s)},
6
- :integer => lambda{|v,opts| v && v.to_i},
7
- :float => lambda{|v,opts| v && v.to_f},
8
- :array => lambda{|v,opts| [*v]},
9
- :boolean => lambda{|v,opts| ['true', '1'].include?(v.to_s.downcase)}
4
+ :string => lambda { |v,opts| v.to_s },
5
+ :time => lambda { |v,opts| v.is_a?(Time) ? v : v && Time.parse(v.to_s) },
6
+ :integer => lambda { |v,opts| v && v.to_i },
7
+ :float => lambda { |v,opts| v && v.to_f },
8
+ :array => lambda { |v,opts| [*v] },
9
+ :boolean => lambda { |v,opts| ['true', '1'].include?(v.to_s.downcase) }
10
10
  }
11
11
  end
12
12
 
@@ -14,7 +14,17 @@ module Cistern::Attributes
14
14
  @transforms ||= {
15
15
  :squash => Proc.new do |k, v, options|
16
16
  squash = options[:squash]
17
- if v.is_a?(::Hash)
17
+ if v.is_a?(::Hash) && squash.is_a?(Array)
18
+ travel = lambda do |tree, path|
19
+ if tree.is_a?(::Hash)
20
+ subtree = tree[path.shift]
21
+ travel.call(subtree, path)
22
+ else tree
23
+ end
24
+ end
25
+
26
+ travel.call(v, squash.dup)
27
+ elsif v.is_a?(::Hash)
18
28
  if v.key?(squash.to_s.to_sym)
19
29
  v[squash.to_s.to_sym]
20
30
  elsif v.has_key?(squash.to_s)
@@ -25,12 +35,12 @@ module Cistern::Attributes
25
35
  else v
26
36
  end
27
37
  end,
28
- :none => lambda{|k, v, opts| v},
38
+ :none => lambda { |k, v, opts| v },
29
39
  }
30
40
  end
31
41
 
32
42
  def self.default_parser
33
- @default_parser ||= lambda{|v, opts| v}
43
+ @default_parser ||= lambda { |v, opts| v }
34
44
  end
35
45
 
36
46
  module ClassMethods
@@ -43,10 +53,12 @@ module Cistern::Attributes
43
53
  end
44
54
 
45
55
  def attributes
46
- @attributes ||= []
56
+ @attributes ||= {}
47
57
  end
48
58
 
49
- def attribute(name, options = {})
59
+ def attribute(_name, options = {})
60
+ name = _name.to_s.to_sym
61
+
50
62
  parser = Cistern::Attributes.parsers[options[:type]] ||
51
63
  options[:parser] ||
52
64
  Cistern::Attributes.default_parser
@@ -62,8 +74,11 @@ module Cistern::Attributes
62
74
  attributes[name.to_s.to_sym]= parser.call(transformed, options)
63
75
  end
64
76
 
65
- @attributes ||= []
66
- @attributes |= [name]
77
+ if self.attributes[name]
78
+ raise(ArgumentError, "#{self.name} attribute[#{_name}] specified more than once")
79
+ else
80
+ self.attributes[name] = options
81
+ end
67
82
 
68
83
  Array(options[:aliases]).each do |new_alias|
69
84
  aliases[new_alias] ||= []
@@ -113,16 +128,22 @@ module Cistern::Attributes
113
128
  end
114
129
 
115
130
  def merge_attributes(new_attributes = {})
116
- for key, value in new_attributes
131
+ new_attributes.each do |key, value|
132
+ # find nested paths
133
+ value.is_a?(::Hash) && self.class.attributes.each do |name, options|
134
+ if (options[:squash] || []).first == key
135
+ send("#{name}=", {key => value})
136
+ end
137
+ end
117
138
  unless self.class.ignored_attributes.include?(key)
118
139
  if self.class.aliases.has_key?(key)
119
140
  self.class.aliases[key].each do |aliased_key|
120
141
  send("#{aliased_key}=", value)
121
142
  end
122
- elsif self.respond_to?("#{key}=", true)
143
+ end
144
+
145
+ if self.respond_to?("#{key}=", true)
123
146
  send("#{key}=", value)
124
- else
125
- # ignore data: unknown attribute : attributes[key] = value
126
147
  end
127
148
  end
128
149
  end
@@ -1,21 +1,14 @@
1
- class Cistern::Collection < Array
1
+ class Cistern::Collection
2
2
  extend Cistern::Attributes::ClassMethods
3
3
  include Cistern::Attributes::InstanceMethods
4
4
 
5
- %w[reject select slice].each do |method|
6
- define_method(method) do |*args, &block|
7
- lazy_load unless @loaded
8
- data = super(*args, &block)
9
- self.clone.clear.concat(data)
10
- end
11
- end
5
+ BLACKLISTED_ARRAY_METHODS = [
6
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
7
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
8
+ :keep_if, :pop, :shift, :delete_at, :compact
9
+ ].to_set # :nodoc:
12
10
 
13
- %w[first last size count to_s].each do |method|
14
- define_method(method) do
15
- lazy_load unless @loaded
16
- super()
17
- end
18
- end
11
+ attr_accessor :records, :loaded, :connection
19
12
 
20
13
  def self.model(new_model=nil)
21
14
  if new_model == nil
@@ -25,18 +18,14 @@ class Cistern::Collection < Array
25
18
  end
26
19
  end
27
20
 
28
- attr_accessor :connection
29
-
30
21
  alias build initialize
31
22
 
32
23
  def initialize(attributes = {})
33
- @loaded = false
34
24
  merge_attributes(attributes)
35
25
  end
36
26
 
37
- def clear
38
- @loaded = true
39
- super
27
+ def all(identity)
28
+ raise NotImplementedError
40
29
  end
41
30
 
42
31
  def create(attributes={})
@@ -47,21 +36,29 @@ class Cistern::Collection < Array
47
36
  raise NotImplementedError
48
37
  end
49
38
 
39
+ def clear
40
+ self.loaded = false
41
+ records && records.clear
42
+ end
43
+
50
44
  def inspect
51
- lazy_load unless @loaded
52
- Cistern.formatter.call(self)
45
+ if Cistern.formatter
46
+ Cistern.formatter.call(self)
47
+ else super
48
+ end
53
49
  end
54
50
 
55
51
  # @api private
56
- def lazy_load
57
- self.all
52
+ def load_records
53
+ self.all unless self.loaded
58
54
  end
59
55
 
56
+ # Should be called within #all to load records into the collection
57
+ # @param [Array<Hash>] objects list of record attributes to be loaded
58
+ # @return self
60
59
  def load(objects)
61
- clear
62
- for object in objects
63
- self << new(object)
64
- end
60
+ self.records = (objects || []).map { |object| new(object) }
61
+ self.loaded = true
65
62
  self
66
63
  end
67
64
 
@@ -83,7 +80,36 @@ class Cistern::Collection < Array
83
80
 
84
81
  def reload
85
82
  clear
86
- lazy_load
83
+ load_records
87
84
  self
88
85
  end
86
+
87
+ def to_a
88
+ load_records
89
+ self.records || []
90
+ end
91
+
92
+ def respond_to?(method, include_private = false)
93
+ super || array_delegable?(method)
94
+ end
95
+
96
+ def ==(comparison_object)
97
+ comparison_object.equal?(self) ||
98
+ (comparison_object.is_a?(self.class) &&
99
+ comparison_object.to_a == self.to_a)
100
+ end
101
+
102
+ protected
103
+
104
+ def array_delegable?(method)
105
+ Array.method_defined?(method) && !BLACKLISTED_ARRAY_METHODS.include?(method)
106
+ end
107
+
108
+ def method_missing(method, *args, &block)
109
+ if array_delegable?(method)
110
+ to_a.public_send(method, *args, &block)
111
+ else
112
+ super
113
+ end
114
+ end
89
115
  end
@@ -32,7 +32,7 @@ module AwesomePrint::Cistern
32
32
  # Format Cistern::Model
33
33
  #------------------------------------------------------------------------------
34
34
  def awesome_cistern_collection(object)
35
- "#{object.class.name} " << awesome_array(object)
35
+ "#{object.class.name} " << awesome_hash(attributes: object.attributes, records: object.records)
36
36
  end
37
37
  end
38
38
 
@@ -15,7 +15,7 @@ module Cistern::Formatter::Formatador
15
15
  Thread.current[:formatador].indent do
16
16
  unless model.class.attributes.empty?
17
17
  data << "\n#{Thread.current[:formatador].indentation}"
18
- data << model.class.attributes.map {|attribute| "#{attribute}=#{model.send(attribute).inspect}"}.join(",\n#{Thread.current[:formatador].indentation}")
18
+ data << model.class.attributes.map { |attribute, _| "#{attribute}=#{model.send(attribute).inspect}" }.join(",\n#{Thread.current[:formatador].indentation}")
19
19
  end
20
20
  end
21
21
  data << "\n#{Thread.current[:formatador].indentation}>"
@@ -8,7 +8,6 @@ module Cistern::Formatter
8
8
  Cistern::Formatter::AwesomePrint
9
9
  elsif defined?(::Formatador)
10
10
  Cistern::Formatter::Formatador
11
- else Cistern::Formatter::Default
12
11
  end
13
12
  end
14
13
  end
data/lib/cistern/mock.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Cistern
2
2
  class Mock
3
- def self.not_implemented
4
- raise NotImplementedError
3
+ def self.not_implemented(method="")
4
+ raise NotImplementedError, method ? "The call '#{method}' is not implemented" : ""
5
5
  end
6
6
 
7
7
  def self.random_hex(length)
data/lib/cistern/model.rb CHANGED
@@ -5,7 +5,11 @@ class Cistern::Model
5
5
  attr_accessor :collection, :connection
6
6
 
7
7
  def inspect
8
- Cistern.formatter.call(self)
8
+ if Cistern.formatter
9
+ Cistern.formatter.call(self)
10
+ else
11
+ "#<#{self.class} #{self.identity}"
12
+ end
9
13
  end
10
14
 
11
15
  def initialize(attributes={})
@@ -28,8 +32,8 @@ class Cistern::Model
28
32
 
29
33
  def ==(comparison_object)
30
34
  comparison_object.equal?(self) ||
31
- (comparison_object.is_a?(self.class) &&
32
- comparison_object.identity == self.identity &&
35
+ (comparison_object.is_a?(self.class) &&
36
+ comparison_object.identity == self.identity &&
33
37
  !comparison_object.new_record?)
34
38
  end
35
39
 
@@ -121,7 +121,7 @@ class Cistern::Service
121
121
  else
122
122
  service::Mock.module_eval <<-EOS, __FILE__, __LINE__
123
123
  def #{request}(*args)
124
- Cistern::Mock.not_implemented
124
+ Cistern::Mock.not_implemented(request)
125
125
  end
126
126
  EOS
127
127
  end
@@ -1,3 +1,3 @@
1
1
  module Cistern
2
- VERSION = "0.3.2"
2
+ VERSION = "0.5.4"
3
3
  end
data/lib/cistern.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'cistern/version'
2
+
2
3
  require 'time'
4
+ require 'set'
3
5
 
4
6
  module Cistern
5
7
 
@@ -15,7 +17,6 @@ module Cistern
15
17
  require 'cistern/service'
16
18
 
17
19
  extend WaitFor
18
- timeout_error = Timeout
19
20
 
20
21
  autoload :Formatter, 'cistern/formatter'
21
22
 
data/spec/cistern_spec.rb CHANGED
@@ -5,25 +5,18 @@ describe "#inspect" do
5
5
  identity :id
6
6
  attribute :name
7
7
  end
8
+
8
9
  class Inspectors < Cistern::Collection
10
+
9
11
  model Inspector
10
12
 
11
- def all
13
+ def all(options={})
14
+ merge_attributes(options)
12
15
  self.load([{id: 1, name: "2"},{id: 3, name: "4"}])
13
16
  end
14
17
  end
15
18
 
16
- before(:all) do
17
- Cistern.formatter = Cistern::Formatter::Default
18
- end
19
-
20
19
  describe "Cistern::Model" do
21
- it "should use default" do
22
- Cistern.formatter = Cistern::Formatter::Default
23
-
24
- Inspector.new(id: 1, name: "name").inspect.should match /#<Inspector:0x[0-9a-f]+ attributes={id:1,name:\"name\"}/
25
- end
26
-
27
20
  it "should use awesome_print" do
28
21
  Cistern.formatter = Cistern::Formatter::AwesomePrint
29
22
 
@@ -41,7 +34,8 @@ describe "#inspect" do
41
34
  end
42
35
 
43
36
  describe "Cistern::Collection" do
44
- it "should use default" do
37
+ it "should use formatador" do
38
+ Cistern.formatter = Cistern::Formatter::Formatador
45
39
  Inspectors.new.all.inspect.should == %q{ <Inspectors
46
40
  [
47
41
  <Inspector
@@ -55,10 +49,10 @@ describe "#inspect" do
55
49
  ]
56
50
  >}
57
51
  end
52
+
58
53
  it "should use awesome_print" do
59
54
  Cistern.formatter = Cistern::Formatter::AwesomePrint
60
- Inspectors.new.all.inspect.should match(/Inspectors\s+\[.*\]$/m) # close enough
55
+ Inspectors.new.all.inspect.should match(/Inspectors\s+{.*}$/m) # close enough
61
56
  end
62
- it "should use formatador"
63
57
  end
64
58
  end
data/spec/model_spec.rb CHANGED
@@ -28,15 +28,18 @@ describe "Cistern::Model" do
28
28
  attribute :list, type: :array
29
29
  attribute :number, type: :integer
30
30
  attribute :floater, type: :float
31
- attribute :butternut, type: :integer, aliases: "squash", squash: "id"
32
- attribute :custom, parser: lambda{|v, opts| "X!#{v}"}
31
+ attribute :butternut_id, squash: ["squash", "id"], type: :integer
32
+ attribute :butternut_type, squash: ["squash", "type"]
33
+ attribute :squash
34
+ attribute :vegetable, aliases: "squash"
35
+ attribute :custom, parser: lambda { |v, _| "X!#{v}" }
33
36
 
34
37
  attribute :same_alias_1, aliases: "nested"
35
38
  attribute :same_alias_2, aliases: "nested"
36
39
 
37
- attribute :same_alias_squashed_1, aliases: "nested", squash: "attr_1"
38
- attribute :same_alias_squashed_2, aliases: "nested", squash: "attr_2"
39
- attribute :same_alias_squashed_3, aliases: "nested", squash: "attr_2"
40
+ attribute :same_alias_squashed_1, squash: ["nested", "attr_1"]
41
+ attribute :same_alias_squashed_2, squash: ["nested", "attr_2"]
42
+ attribute :same_alias_squashed_3, squash: ["nested", "attr_2"]
40
43
 
41
44
  def save
42
45
  requires :flag
@@ -79,8 +82,22 @@ describe "Cistern::Model" do
79
82
  TypeSpec.new(custom: "15").custom.should == "X!15"
80
83
  end
81
84
 
82
- it "should squash and cast" do
83
- TypeSpec.new({"squash" => {"id" => "12"}}).butternut.should == 12
85
+ it "should squash, cast, alias an attribute and keep a vanilla reference" do
86
+ # vanilla squash
87
+ TypeSpec.new({"squash" => {"id" => "12", "type" => "fred"}}).butternut_type.should == "fred"
88
+ TypeSpec.new({"squash" => {"id" => "12", "type" => nil}}).butternut_type.should be_nil
89
+ TypeSpec.new({"squash" => nil}).butternut_type.should be_nil
90
+
91
+ # composite processors: squash and cast
92
+ TypeSpec.new({"squash" => {"id" => "12", "type" => "fred"}}).butternut_id.should == 12
93
+ TypeSpec.new({"squash" => {"id" => nil, "type" => "fred"}}).butternut_id.should be_nil
94
+ TypeSpec.new({"squash" => {"type" => "fred"}}).butternut_id.should be_nil
95
+
96
+ # override intermediate processing
97
+ TypeSpec.new({"squash" => {"id" => "12", "type" => "fred"}}).squash.should == {"id" => "12", "type" => "fred"}
98
+
99
+ # alias of override
100
+ TypeSpec.new({"squash" => {"id" => "12", "type" => "fred"}}).vegetable.should == {"id" => "12", "type" => "fred"}
84
101
  end
85
102
 
86
103
  context "allowing the same alias for multiple attributes" do
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cistern
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
5
- prerelease:
4
+ version: 0.5.4
6
5
  platform: ruby
7
6
  authors:
8
7
  - Josh Lane
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-10-13 00:00:00.000000000 Z
11
+ date: 2014-03-03 00:00:00.000000000 Z
13
12
  dependencies: []
14
13
  description: API client framework extracted from Fog
15
14
  email:
@@ -18,10 +17,11 @@ executables: []
18
17
  extensions: []
19
18
  extra_rdoc_files: []
20
19
  files:
21
- - .gitignore
20
+ - ".gitignore"
21
+ - ".travis.yml"
22
22
  - Gemfile
23
23
  - Guardfile
24
- - LICENSE
24
+ - LICENSE.txt
25
25
  - README.md
26
26
  - Rakefile
27
27
  - TODO.md
@@ -31,7 +31,6 @@ files:
31
31
  - lib/cistern/collection.rb
32
32
  - lib/cistern/formatter.rb
33
33
  - lib/cistern/formatter/awesome_print.rb
34
- - lib/cistern/formatter/default.rb
35
34
  - lib/cistern/formatter/formatador.rb
36
35
  - lib/cistern/hash.rb
37
36
  - lib/cistern/mock.rb
@@ -46,28 +45,28 @@ files:
46
45
  - spec/spec_helper.rb
47
46
  - spec/wait_for_spec.rb
48
47
  homepage: http://joshualane.com/cistern
49
- licenses: []
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
50
51
  post_install_message:
51
52
  rdoc_options: []
52
53
  require_paths:
53
54
  - lib
54
55
  required_ruby_version: !ruby/object:Gem::Requirement
55
- none: false
56
56
  requirements:
57
- - - ! '>='
57
+ - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0'
60
60
  required_rubygems_version: !ruby/object:Gem::Requirement
61
- none: false
62
61
  requirements:
63
- - - ! '>='
62
+ - - ">="
64
63
  - !ruby/object:Gem::Version
65
64
  version: '0'
66
65
  requirements: []
67
66
  rubyforge_project:
68
- rubygems_version: 1.8.25
67
+ rubygems_version: 2.2.2
69
68
  signing_key:
70
- specification_version: 3
69
+ specification_version: 4
71
70
  summary: API client framework
72
71
  test_files:
73
72
  - spec/cistern_spec.rb
@@ -75,4 +74,3 @@ test_files:
75
74
  - spec/model_spec.rb
76
75
  - spec/spec_helper.rb
77
76
  - spec/wait_for_spec.rb
78
- has_rdoc:
@@ -1,5 +0,0 @@
1
- module Cistern::Formatter::Default
2
- def self.call(obj)
3
- "#<%s:0x%x attributes={%s}>" % [obj.class, obj.object_id.abs*2, obj.attributes.map{|k,v| "#{k}:#{v.inspect}"}.join(",")]
4
- end
5
- end