duck_record 0.0.1

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +29 -0
  5. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  6. data/lib/duck_record/attribute.rb +221 -0
  7. data/lib/duck_record/attribute_assignment.rb +91 -0
  8. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  9. data/lib/duck_record/attribute_methods/dirty.rb +124 -0
  10. data/lib/duck_record/attribute_methods/read.rb +78 -0
  11. data/lib/duck_record/attribute_methods/write.rb +65 -0
  12. data/lib/duck_record/attribute_methods.rb +332 -0
  13. data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
  14. data/lib/duck_record/attribute_set/builder.rb +124 -0
  15. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  16. data/lib/duck_record/attribute_set.rb +99 -0
  17. data/lib/duck_record/attributes.rb +262 -0
  18. data/lib/duck_record/base.rb +296 -0
  19. data/lib/duck_record/callbacks.rb +324 -0
  20. data/lib/duck_record/core.rb +253 -0
  21. data/lib/duck_record/define_callbacks.rb +23 -0
  22. data/lib/duck_record/errors.rb +44 -0
  23. data/lib/duck_record/inheritance.rb +130 -0
  24. data/lib/duck_record/locale/en.yml +48 -0
  25. data/lib/duck_record/model_schema.rb +64 -0
  26. data/lib/duck_record/serialization.rb +19 -0
  27. data/lib/duck_record/translation.rb +22 -0
  28. data/lib/duck_record/type/array.rb +36 -0
  29. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  30. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  31. data/lib/duck_record/type/json.rb +6 -0
  32. data/lib/duck_record/type/registry.rb +97 -0
  33. data/lib/duck_record/type/serialized.rb +63 -0
  34. data/lib/duck_record/type/text.rb +9 -0
  35. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  36. data/lib/duck_record/type.rb +66 -0
  37. data/lib/duck_record/validations.rb +40 -0
  38. data/lib/duck_record/version.rb +3 -0
  39. data/lib/duck_record.rb +47 -0
  40. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  41. metadata +126 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ddd659e371a6f19e0c6721e0db7c6b39d84d959
4
+ data.tar.gz: 2ddcf8b3f50ddc3954db3ce04e83d4dbd356a175
5
+ SHA512:
6
+ metadata.gz: 7bc31bc533d951a904ac72d7de80d7398281ad7d701bf3380ab845fd4da6b33f3f491a2f93d2a5599d8461429f501e9fc1822531686c6568c3ede57700206991
7
+ data.tar.gz: cb451b408a562925a2505b3a4282deaedd25d638251d2f49ae5f6d6808911f43a93887735f086a4435b93b177a27b7d74310931d9dd87c6915356857fc0555ba
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Jun Jiang
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ Duck Record
2
+ ====
3
+
4
+ It looks like Active Record and quacks like Active Record, it's Duck Record!
5
+ Actually it's extract from Active Record.
6
+
7
+ ## Usage
8
+
9
+ ```ruby
10
+ class Book < DuckRecord::Base
11
+ attribute :title, :string
12
+ attribute :tags, :string, array: true
13
+ attribute :price, :decimal, default: 0
14
+ attribute :meta, :json, default: {}
15
+ attribute :bought_at, :datetime, default: -> { Time.new }
16
+
17
+ validates :title, presence: true
18
+ end
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ Since Duck Record is under early development,
24
+ I suggest you fetch the gem through GitHub.
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'duck_record', github: 'jasl/duck_record'
30
+ ```
31
+
32
+ And then execute:
33
+ ```bash
34
+ $ bundle
35
+ ```
36
+
37
+ Or install it yourself as:
38
+ ```bash
39
+ $ gem install duck_record
40
+ ```
41
+
42
+ ## TODO
43
+
44
+ - `has_one`, `has_many`
45
+ - refactor that original design for database
46
+ - update docs
47
+ - add tests
48
+
49
+ ## Contributing
50
+
51
+ - Fork the project.
52
+ - Make your feature addition or bug fix.
53
+ - Add tests for it. This is important so I don't break it in a future version unintentionally.
54
+ - Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
55
+ - Send me a pull request. Bonus points for topic branches.
56
+
57
+ ## License
58
+
59
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'DuckRecord'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'lib'
23
+ t.libs << 'test'
24
+ t.pattern = 'test/**/*_test.rb'
25
+ t.verbose = false
26
+ end
27
+
28
+
29
+ task default: :test
@@ -0,0 +1,30 @@
1
+ require 'duck_record/attribute'
2
+
3
+ module DuckRecord
4
+ class Attribute # :nodoc:
5
+ class UserProvidedDefault < FromUser # :nodoc:
6
+ def initialize(name, value, type, database_default)
7
+ @user_provided_value = value
8
+ super(name, value, type, database_default)
9
+ end
10
+
11
+ def value_before_type_cast
12
+ if user_provided_value.is_a?(Proc)
13
+ @memoized_value_before_type_cast ||= user_provided_value.call
14
+ else
15
+ @user_provided_value
16
+ end
17
+ end
18
+
19
+ def with_type(type)
20
+ self.class.new(name, user_provided_value, type, original_attribute)
21
+ end
22
+
23
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
24
+ # Workaround for Ruby 2.2 "private attribute?" warning.
25
+ protected
26
+
27
+ attr_reader :user_provided_value
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,221 @@
1
+ module DuckRecord
2
+ class Attribute # :nodoc:
3
+ class << self
4
+ def from_user(name, value, type, original_attribute = nil)
5
+ FromUser.new(name, value, type, original_attribute)
6
+ end
7
+
8
+ def with_cast_value(name, value, type)
9
+ WithCastValue.new(name, value, type)
10
+ end
11
+
12
+ def null(name)
13
+ Null.new(name)
14
+ end
15
+
16
+ def uninitialized(name, type)
17
+ Uninitialized.new(name, type)
18
+ end
19
+ end
20
+
21
+ attr_reader :name, :value_before_type_cast, :type
22
+
23
+ # This method should not be called directly.
24
+ # Use #from_database or #from_user
25
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
26
+ @name = name
27
+ @value_before_type_cast = value_before_type_cast
28
+ @type = type
29
+ @original_attribute = original_attribute
30
+ end
31
+
32
+ def value
33
+ # `defined?` is cheaper than `||=` when we get back falsy values
34
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
35
+ @value
36
+ end
37
+
38
+ def original_value
39
+ if assigned?
40
+ original_attribute.original_value
41
+ else
42
+ type_cast(value_before_type_cast)
43
+ end
44
+ end
45
+
46
+ def value_for_database
47
+ type.serialize(value)
48
+ end
49
+
50
+ def changed?
51
+ changed_from_assignment? || changed_in_place?
52
+ end
53
+
54
+ def changed_in_place?
55
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
56
+ end
57
+
58
+ def forgetting_assignment
59
+ with_value_from_database(value_for_database)
60
+ end
61
+
62
+ def with_value_from_user(value)
63
+ type.assert_valid_value(value)
64
+ self.class.from_user(name, value, type, original_attribute || self)
65
+ end
66
+
67
+ def with_cast_value(value)
68
+ self.class.with_cast_value(name, value, type)
69
+ end
70
+
71
+ def with_type(type)
72
+ if changed_in_place?
73
+ with_value_from_user(value).with_type(type)
74
+ else
75
+ self.class.new(name, value_before_type_cast, type, original_attribute)
76
+ end
77
+ end
78
+
79
+ def type_cast(*)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def initialized?
84
+ true
85
+ end
86
+
87
+ def came_from_user?
88
+ false
89
+ end
90
+
91
+ def has_been_read?
92
+ defined?(@value)
93
+ end
94
+
95
+ def ==(other)
96
+ self.class == other.class &&
97
+ name == other.name &&
98
+ value_before_type_cast == other.value_before_type_cast &&
99
+ type == other.type
100
+ end
101
+ alias eql? ==
102
+
103
+ def hash
104
+ [self.class, name, value_before_type_cast, type].hash
105
+ end
106
+
107
+ def init_with(coder)
108
+ @name = coder["name"]
109
+ @value_before_type_cast = coder["value_before_type_cast"]
110
+ @type = coder["type"]
111
+ @original_attribute = coder["original_attribute"]
112
+ @value = coder["value"] if coder.map.key?("value")
113
+ end
114
+
115
+ def encode_with(coder)
116
+ coder["name"] = name
117
+ coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast
118
+ coder["type"] = type if type
119
+ coder["original_attribute"] = original_attribute if original_attribute
120
+ coder["value"] = value if defined?(@value)
121
+ end
122
+
123
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
124
+ # Workaround for Ruby 2.2 "private attribute?" warning.
125
+ protected
126
+
127
+ attr_reader :original_attribute
128
+ alias_method :assigned?, :original_attribute
129
+
130
+ def original_value_for_database
131
+ if assigned?
132
+ original_attribute.original_value_for_database
133
+ else
134
+ _original_value_for_database
135
+ end
136
+ end
137
+
138
+ private
139
+ def initialize_dup(other)
140
+ if defined?(@value) && @value.duplicable?
141
+ @value = @value.dup
142
+ end
143
+ end
144
+
145
+ def changed_from_assignment?
146
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
147
+ end
148
+
149
+ def _original_value_for_database
150
+ type.serialize(original_value)
151
+ end
152
+
153
+ class FromUser < Attribute # :nodoc:
154
+ def type_cast(value)
155
+ type.cast(value)
156
+ end
157
+
158
+ def came_from_user?
159
+ true
160
+ end
161
+ end
162
+
163
+ class WithCastValue < Attribute # :nodoc:
164
+ def type_cast(value)
165
+ value
166
+ end
167
+
168
+ def changed_in_place?
169
+ false
170
+ end
171
+ end
172
+
173
+ class Null < Attribute # :nodoc:
174
+ def initialize(name)
175
+ super(name, nil, nil)
176
+ end
177
+
178
+ def type_cast(*)
179
+ nil
180
+ end
181
+
182
+ def with_type(type)
183
+ self.class.with_cast_value(name, nil, type)
184
+ end
185
+
186
+ def with_value_from_user(value)
187
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
188
+ end
189
+ end
190
+
191
+ class Uninitialized < Attribute # :nodoc:
192
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
193
+
194
+ def initialize(name, type)
195
+ super(name, nil, type)
196
+ end
197
+
198
+ def value
199
+ if block_given?
200
+ yield name
201
+ end
202
+ end
203
+
204
+ def original_value
205
+ UNINITIALIZED_ORIGINAL_VALUE
206
+ end
207
+
208
+ def value_for_database
209
+ end
210
+
211
+ def initialized?
212
+ false
213
+ end
214
+
215
+ def with_type(type)
216
+ self.class.new(name, DuckRecord::Type::Value.new)
217
+ end
218
+ end
219
+ private_constant :FromUser, :Null, :Uninitialized, :WithCastValue
220
+ end
221
+ end
@@ -0,0 +1,91 @@
1
+ require 'active_model/forbidden_attributes_protection'
2
+
3
+ module DuckRecord
4
+ module AttributeAssignment
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::AttributeAssignment
7
+
8
+ # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
9
+ def attributes=(attributes)
10
+ assign_attributes(attributes)
11
+ end
12
+
13
+ private
14
+
15
+ def _assign_attributes(attributes)
16
+ multi_parameter_attributes = {}
17
+ nested_parameter_attributes = {}
18
+
19
+ attributes.each do |k, v|
20
+ if k.include?('(')
21
+ multi_parameter_attributes[k] = attributes.delete(k)
22
+ elsif v.is_a?(Hash)
23
+ nested_parameter_attributes[k] = attributes.delete(k)
24
+ end
25
+ end
26
+ super(attributes)
27
+
28
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
29
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
30
+ end
31
+
32
+ # Assign any deferred nested attributes after the base attributes have been set.
33
+ def assign_nested_parameter_attributes(pairs)
34
+ pairs.each { |k, v| _assign_attribute(k, v) }
35
+ end
36
+
37
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
38
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
39
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
40
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
41
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
42
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
43
+ def assign_multiparameter_attributes(pairs)
44
+ execute_callstack_for_multiparameter_attributes(
45
+ extract_callstack_for_multiparameter_attributes(pairs)
46
+ )
47
+ end
48
+
49
+ def execute_callstack_for_multiparameter_attributes(callstack)
50
+ errors = []
51
+ callstack.each do |name, values_with_empty_parameters|
52
+ begin
53
+ if values_with_empty_parameters.each_value.all?(&:nil?)
54
+ values = nil
55
+ else
56
+ values = values_with_empty_parameters
57
+ end
58
+ send("#{name}=", values)
59
+ rescue => ex
60
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
61
+ end
62
+ end
63
+ unless errors.empty?
64
+ error_descriptions = errors.map(&:message).join(",")
65
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
66
+ end
67
+ end
68
+
69
+ def extract_callstack_for_multiparameter_attributes(pairs)
70
+ attributes = {}
71
+
72
+ pairs.each do |(multiparameter_name, value)|
73
+ attribute_name = multiparameter_name.split("(").first
74
+ attributes[attribute_name] ||= {}
75
+
76
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
77
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
78
+ end
79
+
80
+ attributes
81
+ end
82
+
83
+ def type_cast_attribute_value(multiparameter_name, value)
84
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send('to_' + $1) : value
85
+ end
86
+
87
+ def find_parameter_position(multiparameter_name)
88
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
89
+ end
90
+ end
91
+ end