model_attribute 2.0.0 → 2.1.0

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