taxjar-model_attribute 3.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a82866463930e1da3898b2cd7c511dd20b8663d8
4
+ data.tar.gz: 3e0eb679c8ccf1a38565c3a2743de1fc012bf6d7
5
+ SHA512:
6
+ metadata.gz: 48ead77c8ce88fe459c39962205bf3c3262e69328f52d1e28463711f64cc191f78aeecfdac19e9d0d065aa8dd957b9bb72c9c36efdaba10840fd03414493dbcc
7
+ data.tar.gz: d3ff747cad98fe25f477d8d76186556f7e1cb953309ae965374c1d568c592fd3abf777e897d291b0165cee296f2dd84e1aba834afd21baa3e2583f48feadc9d5
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.1.7"
5
+ - "2.2.3"
6
+ - "jruby-9.0.0.0"
@@ -0,0 +1,53 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## 3.0.0
6
+
7
+ - **Breaking change**: All casting errors raise `ArgumentError`. Previously some
8
+ errors during casting would raise `RuntimeError`.
9
+ Thanks to [@gotascii](https://github.com/gotascii) for the report.
10
+
11
+ ## 2.1.0
12
+
13
+ - **New feature**: default values. Allows you to specify a default value like
14
+ so:
15
+ ```
16
+ class User
17
+ attribute :name, :string, default: 'Michelle'
18
+ end
19
+
20
+ User.new.name
21
+ # => 'Michelle'
22
+ ```
23
+
24
+ ## 2.0.0
25
+
26
+ - **Breaking change**: Rename to `ModelAttribute` (no trailing 's') to avoid name
27
+ clash with another gem.
28
+
29
+ ## 1.4.0
30
+
31
+ - **New method**: #changes_for_json Returns a hash from attribute name to its
32
+ new value, suitable for serialization to a JSON string. Easily generate the
33
+ payload to send in an HTTP PUT to a web service.
34
+
35
+ - **New attribute type: json** Store an array/hash/etc. built using the basic
36
+ JSON data types: nil, numeric, string, boolean, hash and array.
37
+
38
+ ## 1.3.0
39
+
40
+ - **Breaking change**: Parsing an integer to a time attribute, the integer is
41
+ treated as the number of milliseconds since the epoch (not the number of
42
+ seconds). `attributes_as_json` emits integers for time attributes.
43
+
44
+ ## 1.2.0
45
+
46
+ - **Breaking change**: `attributes_as_json` removed; replaced with
47
+ `attributes_for_json`. You will have to serialize this yourself:
48
+ `Oj.dump(attributes_for_json, mode: :strict)`. This allows you to modify the
49
+ returned hash before serializing it.
50
+
51
+ ## 1.1.0
52
+
53
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in model_attribute.gemspec
4
+ gemspec
@@ -0,0 +1,47 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec feature)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Make Guard exit when config is changed so it can be restarted
11
+ #
12
+ ## Note: if you want Guard to automatically start up again, run guard in a
13
+ ## shell loop, e.g.:
14
+ #
15
+ # $ while bundle exec guard; do echo "Restarting Guard..."; done
16
+ #
17
+ ## Note: if you are using the `directories` clause above and you are not
18
+ ## watching the project directory ('.'), the you will want to move the Guardfile
19
+ ## to a watched dir and symlink it back, e.g.
20
+ #
21
+ # $ mkdir config
22
+ # $ mv Guardfile config/
23
+ # $ ln -s config/Guardfile .
24
+ #
25
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
26
+ #
27
+ watch ("Guardfile") do
28
+ UI.info "Exiting because Guard must be restarted for changes to take effect"
29
+ exit 0
30
+ end
31
+
32
+ guard :rspec, cmd: "bundle exec rspec --format=Nc --format=documentation", all_on_start: true do
33
+ require "guard/rspec/dsl"
34
+ dsl = Guard::RSpec::Dsl.new(self)
35
+
36
+ # RSpec files
37
+ rspec = dsl.rspec
38
+ watch(rspec.spec_helper) { rspec.spec_dir }
39
+ watch(rspec.spec_support) { rspec.spec_dir }
40
+ watch(rspec.spec_files)
41
+
42
+ # Ruby files
43
+ ruby = dsl.ruby
44
+ dsl.watch_spec_files_for(ruby.lib_files)
45
+
46
+ watch(%r{lib/*}) { 'spec' }
47
+ end
@@ -0,0 +1,26 @@
1
+ ModelAttribute
2
+
3
+ Copyright (c) 2015 Microsoft Corporation
4
+
5
+ All rights reserved.
6
+
7
+ MIT License
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining
10
+ a copy of this software and associated documentation files (the
11
+ "Software"), to deal in the Software without restriction, including
12
+ without limitation the rights to use, copy, modify, merge, publish,
13
+ distribute, sublicense, and/or sell copies of the Software, and to
14
+ permit persons to whom the Software is furnished to do so, subject to
15
+ the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be
18
+ included in all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,223 @@
1
+ # ModelAttribute [![Gem Version](https://badge.fury.io/rb/model_attribute.svg)](http://badge.fury.io/rb/model_attribute) [![Build Status](https://travis-ci.org/yammer/model_attribute.svg?branch=master)](https://travis-ci.org/yammer/model_attribute)
2
+
3
+ Simple attributes for a non-ActiveRecord model.
4
+
5
+ - Stores attributes in instance variables.
6
+ - Type casting and checking.
7
+ - Dirty tracking.
8
+ - List attribute names and values.
9
+ - Default values for attributes
10
+ - Handles integers, booleans, strings and times - a set of types that are very
11
+ easy to persist to and parse from JSON.
12
+ - Supports efficient serialization of attributes to JSON.
13
+ - Mass assignment - handy for initializers.
14
+
15
+ Why not [Virtus][virtus-gem]? Virtus doesn't provide dirty tracking, and
16
+ doesn't integrate with [ActiveModel::Dirty][am-dirty]. So if you're not using
17
+ ActiveRecord, but you need attributes with dirty tracking, ModelAttribute may be
18
+ what you're after. For example, it works very well for a model that fronts an
19
+ HTTP web service, and you want dirty tracking so you can PATCH appropriately.
20
+
21
+ Also in favor of ModelAttribute:
22
+
23
+ - It's simple - less than [200 lines of code][source].
24
+ - It supports efficient serialization and deserialization to/from JSON.
25
+
26
+ [virtus-gem]:https://github.com/solnic/virtus
27
+ [am-dirty]:https://github.com/rails/rails/blob/master/activemodel/lib/active_model/dirty.rb
28
+ [source]:https://github.com/yammer/model_attribute/blob/master/lib/model_attribute.rb
29
+
30
+ ## Usage
31
+
32
+ ```ruby
33
+ require 'model_attribute'
34
+ class User
35
+ extend ModelAttribute
36
+ attribute :id, :integer
37
+ attribute :paid, :boolean
38
+ attribute :name, :string
39
+ attribute :created_at, :time
40
+ attribute :grades, :json
41
+
42
+ def initialize(attributes = {})
43
+ set_attributes(attributes)
44
+ end
45
+ end
46
+
47
+ User.attributes # => [:id, :paid, :name, :created_at, :grades]
48
+ user = User.new
49
+
50
+ user.attributes # => {:id=>nil, :paid=>nil, :name=>nil, :created_at=>nil, :grades=>nil}
51
+
52
+ # An integer attribute
53
+ user.id # => nil
54
+
55
+ user.id = 3
56
+ user.id # => 3
57
+
58
+ # Stores values that convert cleanly to an integer
59
+ user.id = '5'
60
+ user.id # => 5
61
+
62
+ # Protects you against nonsense assignment
63
+ user.id = '5error'
64
+ ArgumentError: invalid value for Integer(): "5error"
65
+
66
+ # A boolean attribute
67
+ user.paid # => nil
68
+ user.paid = true
69
+
70
+ # Booleans also define a predicate method (ending in '?')
71
+ user.paid? # => true
72
+
73
+ # Conversion from strings used by databases.
74
+ user.paid = 'f'
75
+ user.paid # => false
76
+ user.paid = 't'
77
+ user.paid # => true
78
+
79
+ # A :time attribute
80
+ user.created_at = Time.now
81
+ user.created_at # => 2015-01-08 15:57:05 +0000
82
+
83
+ # Also converts from other reasonable time formats
84
+ user.created_at = "2014-12-25 14:00:00 +0100"
85
+ user.created_at # => 2014-12-25 13:00:00 +0000
86
+ user.created_at = Date.parse('2014-01-08')
87
+ user.created_at # => 2014-01-08 00:00:00 +0000
88
+ user.created_at = DateTime.parse("2014-12-25 13:00:45")
89
+ user.created_at # => 2014-12-25 13:00:45 +0000
90
+ # Convert from seconds since the epoch
91
+ user.created_at = Time.now.to_f
92
+ user.created_at # => 2015-01-08 16:23:02 +0000
93
+ # Or milliseconds since the epoch
94
+ user.created_at = 1420734182000
95
+ user.created_at # => 2015-01-08 16:23:02 +0000
96
+
97
+ # A :json attribute is schemaless and accepts the basic JSON types - hash,
98
+ # array, nil, numeric, string and boolean.
99
+ user.grades = {'maths' => 'A', 'history' => 'C'}
100
+ user.grades # => {"maths"=>"A", "history"=>"C"}
101
+ user.grades = ['A', 'A*', 'C']
102
+ user.grades # => ["A", "A*", "C"]
103
+ user.grades = 'AAB'
104
+ user.grades # => "AAB"
105
+ user.grades = Time.now
106
+ # => ArgumentError: JSON only supports nil, numeric, string, boolean and arrays and hashes of those.
107
+
108
+ # read_attribute and write_attribute methods
109
+ user.read_attribute(:created_at)
110
+ user.write_attribute(:name, 'Fred')
111
+
112
+ # View attributes
113
+ user.attributes # => {:id=>5, :paid=>true, :name=>"Fred", :created_at=>2015-01-08 15:57:05 +0000, :grades=>{"maths"=>"A", "history"=>"C"}}
114
+ user.inspect # => "#<User id: 5, paid: true, name: \"Fred\", created_at: 2015-01-08 15:57:05 +0000, grades: {\"maths\"=>\"A\", \"history\"=>\"C\"}>"
115
+
116
+ # Mass assignment
117
+ user.set_attributes(name: "Sally", paid: false)
118
+ user.attributes # => {:id=>5, :paid=>false, :name=>"Sally", :created_at=>2015-01-08 15:57:05 +0000}
119
+
120
+ # Efficient JSON serialization and deserialization.
121
+ # Attributes with nil values are omitted.
122
+ user.attributes_for_json
123
+ # => {"id"=>5, "paid"=>true, "name"=>"Fred", "created_at"=>1421171317762}
124
+ require 'oj'
125
+ Oj.dump(user.attributes_for_json, mode: :strict)
126
+ # => "{\"id\":5,\"paid\":true,\"name\":\"Fred\",\"created_at\":1421171317762}"
127
+ user2 = User.new(Oj.load(json, strict: true))
128
+
129
+ # Change tracking. A much smaller set of functions than that provided by
130
+ # ActiveModel::Dirty.
131
+ user.changes # => {:id=>[nil, 5], :paid=>[nil, true], :created_at=>[nil, 2015-01-08 15:57:05 +0000], :name=>[nil, "Fred"]}
132
+ user.name_changed? # => true
133
+ # If you need the new values to send as a PUT to a web service
134
+ user.changes_for_json # => {"id"=>5, "paid"=>true, "name"=>"Fred", "created_at"=>1421171317762}
135
+ # If you're imitating ActiveRecord behaviour, changes are cleared after
136
+ # after_save callbacks, but before after_commit callbacks.
137
+ user.changes.clear
138
+ user.changes # => {}
139
+
140
+ # Equality if all the attribute values match
141
+ another = User.new
142
+ another.id = 5
143
+ another.paid = true
144
+ another.created_at = user.created_at
145
+ another.name = 'Fred'
146
+
147
+ user == another # => true
148
+ user === another # => true
149
+ user.eql? another # => true
150
+
151
+ # Making some attributes private
152
+
153
+ class User
154
+ extend ModelAttribute
155
+ attribute :events, :string
156
+ private :events=
157
+
158
+ def initialize(attributes)
159
+ # Pass flag to set_attributes to allow setting attributes with private writers
160
+ set_attributes(attributes, true)
161
+ end
162
+
163
+ def add_event(new_event)
164
+ events ||= ""
165
+ events += new_event
166
+ end
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"}
193
+ ```
194
+
195
+ ## Installation
196
+
197
+ Add this line to your application's Gemfile:
198
+
199
+ ```ruby
200
+ gem 'model_attribute'
201
+ ```
202
+
203
+ And then execute:
204
+
205
+ $ bundle
206
+
207
+ Or install it yourself as:
208
+
209
+ $ gem install model_attribute
210
+
211
+ ## Testing
212
+
213
+ Running specs:
214
+
215
+ $ TZ=UTC rspec
216
+
217
+ ## Contributing
218
+
219
+ 1. [Fork it](https://github.com/yammer/model_attribute/fork)
220
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
221
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
222
+ 4. Push to the branch (`git push origin my-new-feature`)
223
+ 5. Create a new Pull Request
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,163 @@
1
+ require "model_attribute/version"
2
+ require "model_attribute/casts"
3
+ require "model_attribute/errors"
4
+ require "time"
5
+
6
+ module ModelAttribute
7
+ SUPPORTED_TYPES = [:integer, :float, :boolean, :string, :time, :json]
8
+
9
+ def self.extended(base)
10
+ base.send(:include, InstanceMethods)
11
+ base.instance_variable_set('@attribute_names', [])
12
+ base.instance_variable_set('@attribute_types', {})
13
+ base.instance_variable_set('@attribute_defaults', {})
14
+ end
15
+
16
+ def attribute(name, type, opts = {})
17
+ name = name.to_sym
18
+ type = type.to_sym
19
+ raise UnsupportedTypeError.new(type) unless SUPPORTED_TYPES.include?(type)
20
+
21
+ @attribute_names << name
22
+ @attribute_types[name] = type
23
+ @attribute_defaults[name] = opts[:default] if opts.key?(:default)
24
+
25
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
26
+ def #{name}=(value)
27
+ write_attribute(#{name.inspect}, value, #{type.inspect})
28
+ end
29
+
30
+ def #{name}
31
+ read_attribute(#{name.inspect})
32
+ end
33
+
34
+ def #{name}_changed?
35
+ !!changes[#{name.inspect}]
36
+ end
37
+ CODE
38
+
39
+ if type == :boolean
40
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
41
+ def #{name}?
42
+ !!read_attribute(#{name.inspect})
43
+ end
44
+ CODE
45
+ end
46
+ end
47
+
48
+ def attributes
49
+ @attribute_names
50
+ end
51
+
52
+ def attribute_defaults
53
+ @attribute_defaults
54
+ end
55
+
56
+ module InstanceMethods
57
+ def write_attribute(name, value, type = nil)
58
+ name = name.to_sym
59
+
60
+ # Don't want to expose attribute types as a method on the class, so access
61
+ # via a back door.
62
+ type ||= self.class.instance_variable_get('@attribute_types')[name]
63
+ raise InvalidAttributeNameError.new(name) unless type
64
+
65
+ value = Casts.cast(value, type)
66
+ return if value == read_attribute(name)
67
+
68
+ if changes.has_key? name
69
+ original = changes[name].first
70
+ else
71
+ original = read_attribute(name)
72
+ end
73
+
74
+ if original == value
75
+ changes.delete(name)
76
+ else
77
+ changes[name] = [original, value]
78
+ end
79
+
80
+ instance_variable_set("@#{name}", value)
81
+ end
82
+
83
+ def read_attribute(name)
84
+ ivar_name = "@#{name}"
85
+ if instance_variable_defined?(ivar_name)
86
+ instance_variable_get(ivar_name)
87
+ elsif !self.class.attributes.include?(name.to_sym)
88
+ raise InvalidAttributeNameError.new(name)
89
+ else
90
+ self.class.attribute_defaults[name.to_sym]
91
+ end
92
+ end
93
+
94
+ def attributes
95
+ self.class.attributes.each_with_object({}) do |name, attributes|
96
+ attributes[name] = read_attribute(name)
97
+ end
98
+ end
99
+
100
+ def set_attributes(attributes, can_set_private_attrs = false)
101
+ attributes.each do |key, value|
102
+ send("#{key}=", value) if respond_to?("#{key}=", can_set_private_attrs)
103
+ end
104
+ end
105
+
106
+ def ==(other)
107
+ return true if equal?(other)
108
+ if respond_to?(:id)
109
+ other.kind_of?(self.class) && id == other.id
110
+ else
111
+ other.kind_of?(self.class) && attributes == other.attributes
112
+ end
113
+ end
114
+ alias_method :eql?, :==
115
+
116
+ def changes
117
+ @changes ||= {}
118
+ end
119
+
120
+ # Attributes suitable for serializing to a JSON string.
121
+ #
122
+ # - Attribute keys are strings (for 'strict' JSON dumping).
123
+ # - Attributes with a default or nil value are omitted to speed serialization.
124
+ # - :time attributes are serialized as an Integer giving the number of
125
+ # milliseconds since the epoch.
126
+ def attributes_for_json
127
+ self.class.attributes.each_with_object({}) do |name, attributes|
128
+ value = read_attribute(name)
129
+ if value != self.class.attribute_defaults[name.to_sym]
130
+ value = (value.to_f * 1000).to_i if value.is_a? Time
131
+ attributes[name.to_s] = value
132
+ end
133
+ end
134
+ end
135
+
136
+ # Changed attributes suitable for serializing to a JSON string. Returns a
137
+ # hash from attribute name (as a string) to the new value of that attribute,
138
+ # for attributes that have changed.
139
+ #
140
+ # - :time attributes are serialized as an Integer giving the number of
141
+ # milliseconds since the epoch.
142
+ # - Unlike attributes_for_json, attributes that have changed to a nil value
143
+ # *are* included.
144
+ def changes_for_json
145
+ hash = {}
146
+ changes.each do |attr_name, (_old_value, new_value)|
147
+ new_value = (new_value.to_f * 1000).to_i if new_value.is_a? Time
148
+ hash[attr_name.to_s] = new_value
149
+ end
150
+
151
+ hash
152
+ end
153
+
154
+ # Includes the class name and all the attributes and their values. e.g.
155
+ # "#<User id: 1, paid: true, name: \"Fred\", created_at: 2014-12-25 08:00:00 +0000>"
156
+ def inspect
157
+ attribute_string = self.class.attributes.map do |key|
158
+ "#{key}: #{read_attribute(key).inspect}"
159
+ end.join(', ')
160
+ "#<#{self.class} #{attribute_string}>"
161
+ end
162
+ end
163
+ end