model_attribute 2.0.0 → 2.1.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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZjY2MDhkZDI2ZjA0YjRjM2NkY2E5MTg0NWYxZmJhY2JmZTEyYWUzYg==
4
+ ZjQzN2Q4YWFiMzRhYTA1NTI0Y2RkNDNjOGZlZjg4Zjk3ZTU1NzlkMQ==
5
5
  data.tar.gz: !binary |-
6
- MWRhZjRhOTUzNDQ5MzRhMWU5NTQwOTdkMmJjYmJlMDA1OTJhMzFiMA==
6
+ ZTc1NmViMDUxNDhiNmIzMGJiYmNiMzE5ZWM2ZDFjMGVmM2EwNjQxNg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZjZmMjY3NzMzM2EyODAyMDM1MTA2NTgxNGRhOWJhNGNmMGJmZTA4NDVmMzJi
10
- NWQ0MzEyYTFlZDNjZmI5N2I5Y2FjMDE3NWE0MDIyMDBlNWJmMzNkNzNmMjBh
11
- YTQ2Y2MzMjU2ZGI4MzNhODE5NDJkMjEyMmZmNjU3NDUwYjAzNTI=
9
+ MzM1MDY3M2Q0ZDE4MWViNjcwNjY4ZjI5OTg1ZjJkYjY5N2JlNWM2MmQyMTUx
10
+ ODRjZWVmNDI2NzIxODk4MmY0YmQ0MTY3NGM0OTk5OGExMTFjY2UwM2Q0MzQ2
11
+ MzMyY2FlM2M3ZDk5Zjc3YjUzYTY5NmIwNjQ3NjU1NjYxYjMyMWM=
12
12
  data.tar.gz: !binary |-
13
- ODg1MWVhYjI3N2ZmNDM0MWY1ZGE1NWVmYWM3YzI4ODdiZTEzMGY0MDQ5YjRh
14
- YjA3MWNmMjlmYTJmNTU4ZjJjNzk2ZmUwOWRjZjhhMGJkYTUzNGZlZDZmZGNh
15
- N2VhZGJjYWE1MzgxYjY4ZDQwNWI4YmE5ZGY0OWNjZjk4YmNjMGI=
13
+ MmE3NmY2MmMyYmU3MmRiNzY5ZGRhNjQ3MzYxMDhlMzdiZjhlZTFiMDVhOTY3
14
+ MDU0M2Q1OGJjZjRkZGM5YjA3ZGYyY2E0OTBkNDUwMDAxM2Y1NmQ2MTUzOWZi
15
+ N2VkMmM5MzViMGQ3OWYyODljNTg3Y2E4ZGExZTVhMmQzMzgzODc=
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.1.5"
5
+ - "2.2.1"
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 2.1.0
6
+
7
+ - **New feature**: default values. Allows you to specify a default value like
8
+ so:
9
+ ```
10
+ class User
11
+ attribute :name, :string, default: 'Michelle'
12
+ end
13
+
14
+ User.new.name
15
+ # => 'Michelle'
16
+ ```
17
+
5
18
  ## 2.0.0
6
19
 
7
20
  - **Breaking change**: Rename to `ModelAttribute` (no trailing 's') to avoid name
data/Gemfile CHANGED
@@ -1,5 +1,4 @@
1
1
  source 'https://rubygems.org'
2
- source 'http://gems.int.yammer.com/'
3
2
 
4
3
  # Specify your gem's dependencies in model_attribute.gemspec
5
4
  gemspec
data/Guardfile CHANGED
@@ -29,7 +29,7 @@ watch ("Guardfile") do
29
29
  exit 0
30
30
  end
31
31
 
32
- guard :rspec, cmd: "bundle exec rspec --format=Nc --format=documentation" do
32
+ guard :rspec, cmd: "bundle exec rspec --format=Nc --format=documentation", all_on_start: true do
33
33
  require "guard/rspec/dsl"
34
34
  dsl = Guard::RSpec::Dsl.new(self)
35
35
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ModelAttribute
1
+ # ModelAttribute [![Build Status](https://travis-ci.org/yammer/model_attribute.svg?branch=master)](https://travis-ci.org/yammer/model_attribute)
2
2
 
3
3
  Simple attributes for a non-ActiveRecord model.
4
4
 
@@ -6,6 +6,7 @@ Simple attributes for a non-ActiveRecord model.
6
6
  - Type casting and checking.
7
7
  - Dirty tracking.
8
8
  - List attribute names and values.
9
+ - Default values for attributes
9
10
  - Handles integers, booleans, strings and times - a set of types that are very
10
11
  easy to persist to and parse from JSON.
11
12
  - Supports efficient serialization of attributes to JSON.
@@ -164,6 +165,31 @@ class User
164
165
  events += new_event
165
166
  end
166
167
  end
168
+
169
+ # Supporting default attributes
170
+
171
+ class UserWithDefaults
172
+ extend ModelAttribute
173
+
174
+ attribute :name, :string, default: 'Charlie'
175
+ end
176
+
177
+ UserWithDefaults.attribute_defaults # => {:name=>"Charlie"}
178
+
179
+ user = UserWithDefaults.new
180
+ user.name # => "Charlie"
181
+ user.read_attribute(:name) # => "Charlie"
182
+ user.attributes # => {:name=>"Charlie"}
183
+ # attributes_for_json omits defaults to keep the JSON compact
184
+ user.attributes_for_json # => {}
185
+ # you can add them back in if you need them
186
+ user.attributes_for_json.merge(user.class.attribute_defaults) # => {:name=>"Charlie"}
187
+ # A default isn't a change
188
+ user.changes # => {}
189
+ user.changes_for_json # => {}
190
+
191
+ user.name = 'Bob'
192
+ user.attributes # => {:name=>"Bob"}
167
193
  ```
168
194
 
169
195
  ## Installation
data/Rakefile CHANGED
@@ -1 +1,8 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec) do |t|
4
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
5
+ t.rspec_opts = '--format documentation'
6
+ end
7
+
8
+ task :default => :spec
@@ -1,5 +1,5 @@
1
1
  require "model_attribute/version"
2
- require "model_attribute/json"
2
+ require "model_attribute/casts"
3
3
  require "model_attribute/errors"
4
4
  require "time"
5
5
 
@@ -8,17 +8,19 @@ module ModelAttribute
8
8
 
9
9
  def self.extended(base)
10
10
  base.send(:include, InstanceMethods)
11
- base.instance_variable_set('@attribute_names', [])
12
- base.instance_variable_set('@attribute_types', {})
11
+ base.instance_variable_set('@attribute_names', [])
12
+ base.instance_variable_set('@attribute_types', {})
13
+ base.instance_variable_set('@attribute_defaults', {})
13
14
  end
14
15
 
15
- def attribute(name, type)
16
+ def attribute(name, type, opts = {})
16
17
  name = name.to_sym
17
18
  type = type.to_sym
18
19
  raise UnsupportedTypeError.new(type) unless SUPPORTED_TYPES.include?(type)
19
20
 
20
- @attribute_names << name
21
- @attribute_types[name] = type
21
+ @attribute_names << name
22
+ @attribute_types[name] = type
23
+ @attribute_defaults[name] = opts[:default] if opts.key?(:default)
22
24
 
23
25
  self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
24
26
  def #{name}=(value)
@@ -47,6 +49,10 @@ module ModelAttribute
47
49
  @attribute_names
48
50
  end
49
51
 
52
+ def attribute_defaults
53
+ @attribute_defaults
54
+ end
55
+
50
56
  module InstanceMethods
51
57
  def write_attribute(name, value, type = nil)
52
58
  name = name.to_sym
@@ -56,7 +62,7 @@ module ModelAttribute
56
62
  type ||= self.class.instance_variable_get('@attribute_types')[name]
57
63
  raise InvalidAttributeNameError.new(name) unless type
58
64
 
59
- value = cast(value, type)
65
+ value = Casts.cast(value, type)
60
66
  return if value == read_attribute(name)
61
67
 
62
68
  if changes.has_key? name
@@ -80,6 +86,8 @@ module ModelAttribute
80
86
  instance_variable_get(ivar_name)
81
87
  elsif !self.class.attributes.include?(name.to_sym)
82
88
  raise InvalidAttributeNameError.new(name)
89
+ else
90
+ self.class.attribute_defaults[name.to_sym]
83
91
  end
84
92
  end
85
93
 
@@ -106,19 +114,19 @@ module ModelAttribute
106
114
  alias_method :eql?, :==
107
115
 
108
116
  def changes
109
- @changes ||= {} #HashWithIndifferentAccess.new
117
+ @changes ||= {}
110
118
  end
111
119
 
112
120
  # Attributes suitable for serializing to a JSON string.
113
121
  #
114
122
  # - Attribute keys are strings (for 'strict' JSON dumping).
115
- # - Attributes with a nil value are omitted to speed serialization.
123
+ # - Attributes with a default or nil value are omitted to speed serialization.
116
124
  # - :time attributes are serialized as an Integer giving the number of
117
125
  # milliseconds since the epoch.
118
126
  def attributes_for_json
119
127
  self.class.attributes.each_with_object({}) do |name, attributes|
120
128
  value = read_attribute(name)
121
- unless value.nil?
129
+ if value != self.class.attribute_defaults[name.to_sym]
122
130
  value = (value.to_f * 1000).to_i if value.is_a? Time
123
131
  attributes[name.to_s] = value
124
132
  end
@@ -151,52 +159,5 @@ module ModelAttribute
151
159
  end.join(', ')
152
160
  "#<#{self.class} #{attribute_string}>"
153
161
  end
154
-
155
- def cast(value, type)
156
- return nil if value.nil?
157
-
158
- case type
159
- when :integer
160
- int = Integer(value)
161
- float = Float(value)
162
- raise "Can't cast #{value.inspect} to an integer without loss of precision" unless int == float
163
- int
164
- when :boolean
165
- if !!value == value
166
- value
167
- elsif value == 't'
168
- true
169
- elsif value == 'f'
170
- false
171
- else
172
- raise "Can't cast #{value.inspect} to boolean"
173
- end
174
- when :time
175
- case value
176
- when Time
177
- value
178
- when Date, DateTime
179
- value.to_time
180
- when Integer
181
- # Assume milliseconds since epoch.
182
- Time.at(value / 1000.0)
183
- when Numeric
184
- # Numeric, but not an integer. Assume seconds since epoch.
185
- Time.at(value)
186
- else
187
- Time.parse(value)
188
- end
189
- when :string
190
- String(value)
191
- when :json
192
- if Json.valid?(value)
193
- value
194
- else
195
- raise "JSON only supports nil, numeric, string, boolean and arrays and hashes of those."
196
- end
197
- else
198
- raise UnsupportedTypeError.new(type)
199
- end
200
- end
201
162
  end
202
163
  end
@@ -0,0 +1,74 @@
1
+ module ModelAttribute
2
+ module Casts
3
+ class << self
4
+ def cast(value, type)
5
+ return nil if value.nil?
6
+
7
+ case type
8
+ when :integer
9
+ int = Integer(value)
10
+ float = Float(value)
11
+ raise "Can't cast #{value.inspect} to an integer without loss of precision" unless int == float
12
+ int
13
+ when :boolean
14
+ if !!value == value
15
+ value
16
+ elsif value == 't'
17
+ true
18
+ elsif value == 'f'
19
+ false
20
+ else
21
+ raise "Can't cast #{value.inspect} to boolean"
22
+ end
23
+ when :time
24
+ case value
25
+ when Time
26
+ value
27
+ when Date, DateTime
28
+ value.to_time
29
+ when Integer
30
+ # Assume milliseconds since epoch.
31
+ Time.at(value / 1000.0)
32
+ when Numeric
33
+ # Numeric, but not an integer. Assume seconds since epoch.
34
+ Time.at(value)
35
+ else
36
+ Time.parse(value)
37
+ end
38
+ when :string
39
+ String(value)
40
+ when :json
41
+ if valid_json?(value)
42
+ value
43
+ else
44
+ raise "JSON only supports nil, numeric, string, boolean and arrays and hashes of those."
45
+ end
46
+ else
47
+ raise UnsupportedTypeError.new(type)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def valid_json?(value)
54
+ (value == nil ||
55
+ value == true ||
56
+ value == false ||
57
+ value.is_a?(Numeric) ||
58
+ value.is_a?(String) ||
59
+ (value.is_a?(Array) && valid_json_array?(value)) ||
60
+ (value.is_a?(Hash) && valid_json_hash?(value) ))
61
+ end
62
+
63
+ def valid_json_array?(array)
64
+ array.all? { |value| valid_json?(value) }
65
+ end
66
+
67
+ def valid_json_hash?(hash)
68
+ hash.all? do |key, value|
69
+ key.is_a?(String) && valid_json?(value)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,3 @@
1
1
  module ModelAttribute
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -9,6 +9,10 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["David Waller"]
10
10
  spec.email = ["dwaller@yammer-inc.com"]
11
11
  spec.summary = %q{Attributes for non-ActiveRecord models}
12
+ spec.description = <<-EOF
13
+ Attributes for non-ActiveRecord models.
14
+ Smaller and simpler than Virtus, and adds dirty tracking.
15
+ EOF
12
16
  spec.homepage = ""
13
17
  spec.license = "MIT"
14
18
 
@@ -23,5 +27,4 @@ Gem::Specification.new do |spec|
23
27
  spec.add_development_dependency "rspec-nc", "~> 0.2"
24
28
  spec.add_development_dependency "guard", "~> 2.8"
25
29
  spec.add_development_dependency "guard-rspec", "~> 4.3"
26
- spec.add_development_dependency "pry-debugger"
27
30
  end
@@ -1,10 +1,11 @@
1
1
  class User
2
2
  extend ModelAttribute
3
- attribute :id, :integer
4
- attribute :paid, :boolean
5
- attribute :name, :string
6
- attribute :created_at, :time
7
- attribute :profile, :json
3
+ attribute :id, :integer
4
+ attribute :paid, :boolean
5
+ attribute :name, :string
6
+ attribute :created_at, :time
7
+ attribute :profile, :json
8
+ attribute :reward_points, :integer, default: 0
8
9
 
9
10
  def initialize(attributes = {})
10
11
  set_attributes(attributes)
@@ -23,19 +24,29 @@ class UserWithoutId
23
24
  end
24
25
 
25
26
  RSpec.describe "a class using ModelAttribute" do
26
- describe ".attributes" do
27
- it "returns an array of attribute names as symbols" do
28
- expect(User.attributes).to eq([:id, :paid, :name, :created_at, :profile])
27
+ describe "class methods" do
28
+ describe ".attribute" do
29
+ context "passed an unrecognised type" do
30
+ it "raises an error" do
31
+ expect do
32
+ User.attribute :address, :custom_type
33
+ end.to raise_error(ModelAttribute::UnsupportedTypeError,
34
+ "Unsupported type :custom_type. " +
35
+ "Must be one of :integer, :boolean, :string, :time, :json.")
36
+ end
37
+ end
29
38
  end
30
- end
31
39
 
32
- describe "defining an attribute with an invalid type" do
33
- it "raises an error" do
34
- expect do
35
- User.attribute :address, :custom_type
36
- end.to raise_error(ModelAttribute::UnsupportedTypeError,
37
- "Unsupported type :custom_type. " +
38
- "Must be one of :integer, :boolean, :string, :time, :json.")
40
+ describe ".attributes" do
41
+ it "returns an array of attribute names as symbols" do
42
+ expect(User.attributes).to eq([:id, :paid, :name, :created_at, :profile, :reward_points])
43
+ end
44
+ end
45
+
46
+ describe ".attribute_defaults" do
47
+ it "returns a hash of attributes that have non-nil defaults" do
48
+ expect(User.attribute_defaults).to eq({reward_points: 0})
49
+ end
39
50
  end
40
51
  end
41
52
 
@@ -295,6 +306,12 @@ RSpec.describe "a class using ModelAttribute" do
295
306
  end
296
307
  end
297
308
 
309
+ describe 'a defaulted attribute (reward_points)' do
310
+ it "returns the default when unset" do
311
+ expect(user.reward_points).to eq(0)
312
+ end
313
+ end
314
+
298
315
  describe "#write_attribute" do
299
316
  it "does the same casting as using the writer method" do
300
317
  user.write_attribute(:id, '3')
@@ -319,6 +336,12 @@ RSpec.describe "a class using ModelAttribute" do
319
336
  expect(user.read_attribute(:id)).to be_nil
320
337
  end
321
338
 
339
+ context "for an attribute with a default" do
340
+ it "returns the default if the attribute has not been set" do
341
+ expect(user.read_attribute(:reward_points)).to eq(0)
342
+ end
343
+ end
344
+
322
345
  it "raises an error if passed an invalid attribute name" do
323
346
  expect do
324
347
  user.read_attribute(:spelling_mistake)
@@ -330,7 +353,7 @@ RSpec.describe "a class using ModelAttribute" do
330
353
  describe "#changes" do
331
354
  let(:changes) { user.changes }
332
355
 
333
- context "for a model instance created with no attributes" do
356
+ context "for a model instance created with no attributes except defaults" do
334
357
  it "is empty" do
335
358
  expect(changes).to be_empty
336
359
  end
@@ -408,7 +431,7 @@ RSpec.describe "a class using ModelAttribute" do
408
431
  end
409
432
  end
410
433
 
411
- describe "id_changed?" do
434
+ describe "#id_changed?" do
412
435
  context "with no changes" do
413
436
  it "returns false" do
414
437
  expect(user.id_changed?).to eq(false)
@@ -470,8 +493,18 @@ RSpec.describe "a class using ModelAttribute" do
470
493
  expect(user.attributes_for_json).to include("profile" => json)
471
494
  end
472
495
 
473
- it "omits attributes with a nil value" do
474
- expect(user.attributes_for_json).to_not include("name")
496
+ it "omits attributes still set to the default value" do
497
+ expect(user.attributes_for_json).to_not include("name", "reward_points")
498
+ end
499
+
500
+ it "includes an attribute changed from its default value" do
501
+ user.name = "Fred"
502
+ expect(user.attributes_for_json).to include("name" => "Fred")
503
+ end
504
+
505
+ it "includes an attribute changed from its default value to nil" do
506
+ user.reward_points = nil
507
+ expect(user.attributes_for_json).to include("reward_points" => nil)
475
508
  end
476
509
  end
477
510
 
@@ -531,12 +564,16 @@ RSpec.describe "a class using ModelAttribute" do
531
564
  expect(user.inspect).to include('profile: {"interests"=>["coding", "social networks"], "rank"=>15}')
532
565
  end
533
566
 
567
+ it "includes defaulted attributes" do
568
+ expect(user.inspect).to include('reward_points: 0')
569
+ end
570
+
534
571
  it "includes the class name" do
535
572
  expect(user.inspect).to include("User")
536
573
  end
537
574
 
538
575
  it "looks like '#<User id: 1, paid: true, name: ..., created_at: ...>'" do
539
- expect(user.inspect).to eq("#<User id: 1, paid: true, name: \"Fred\", created_at: 2014-12-25 08:00:00 +0000, profile: {\"interests\"=>[\"coding\", \"social networks\"], \"rank\"=>15}>")
576
+ expect(user.inspect).to eq("#<User id: 1, paid: true, name: \"Fred\", created_at: 2014-12-25 08:00:00 +0000, profile: {\"interests\"=>[\"coding\", \"social networks\"], \"rank\"=>15}, reward_points: 0>")
540
577
  end
541
578
  end
542
579
 
@@ -1,5 +1,4 @@
1
1
  require 'model_attribute'
2
- require 'pry-debugger'
3
2
 
4
3
  # This file was generated by the `rspec --init` command. Conventionally, all
5
4
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model_attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Waller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-26 00:00:00.000000000 Z
11
+ date: 2015-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,21 +94,8 @@ dependencies:
94
94
  - - ~>
95
95
  - !ruby/object:Gem::Version
96
96
  version: '4.3'
97
- - !ruby/object:Gem::Dependency
98
- name: pry-debugger
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ! '>='
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ! '>='
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- description:
97
+ description: ! " Attributes for non-ActiveRecord models.\n Smaller and simpler
98
+ than Virtus, and adds dirty tracking.\n"
112
99
  email:
113
100
  - dwaller@yammer-inc.com
114
101
  executables: []
@@ -117,6 +104,7 @@ extra_rdoc_files: []
117
104
  files:
118
105
  - .gitignore
119
106
  - .rspec
107
+ - .travis.yml
120
108
  - CHANGELOG.md
121
109
  - Gemfile
122
110
  - Guardfile
@@ -124,8 +112,8 @@ files:
124
112
  - README.md
125
113
  - Rakefile
126
114
  - lib/model_attribute.rb
115
+ - lib/model_attribute/casts.rb
127
116
  - lib/model_attribute/errors.rb
128
- - lib/model_attribute/json.rb
129
117
  - lib/model_attribute/version.rb
130
118
  - model_attribute.gemspec
131
119
  - performance_comparison.rb
@@ -1,27 +0,0 @@
1
- module ModelAttribute
2
- module Json
3
- class << self
4
- def valid?(value)
5
- (value == nil ||
6
- value == true ||
7
- value == false ||
8
- value.is_a?(Numeric) ||
9
- value.is_a?(String) ||
10
- (value.is_a?(Array) && valid_array?(value)) ||
11
- (value.is_a?(Hash) && valid_hash?(value) ))
12
- end
13
-
14
- private
15
-
16
- def valid_array?(array)
17
- array.all? { |value| valid?(value) }
18
- end
19
-
20
- def valid_hash?(hash)
21
- hash.all? do |key, value|
22
- key.is_a?(String) && valid?(value)
23
- end
24
- end
25
- end
26
- end
27
- end