options_model 0.0.1

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: bbc86cf05d36942bfaedc8e5110ffde4560dec33
4
+ data.tar.gz: 51d723a133527412281c1751da1269cff2e6eb13
5
+ SHA512:
6
+ metadata.gz: 349f91af8fe2bdc272af7fd56ba0f307a798e9bfc14b13c28216ea0ca5c07551cc6fecba7f130171e61328c84352c9415cdcd66a897b3eed276d6f4834d39b9c
7
+ data.tar.gz: 41fb67f98b8ca797a0a7d97985694eceadbc419dd03db1141cecf07643efd81eb9227c4e6530d8890f53ffde9a93390f015082725987e90d30a92b3a31af5349
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,56 @@
1
+ # OptionsModel
2
+
3
+ An ActiveModel implementation that make easier to handle model which will be serialized in a real model.
4
+
5
+ support attribute:
6
+
7
+ - all types that `ActiveModel::Type` support
8
+ - typed array
9
+ - enum
10
+ - embeds one
11
+
12
+ ## Usage
13
+
14
+ class Person < OptionsModel::Base
15
+ attribute :name, :string
16
+ attribute :age, :integer
17
+
18
+ validates :name, presence: true
19
+ end
20
+
21
+ class Book < OptionsModel::Base
22
+ embeds_one :author, class_name: 'Person'
23
+
24
+ attribute :title, :string
25
+ attribute :tags, :string, array: true
26
+ attribute :price, :decimal, default: 0
27
+ attribute :meta, :json, default: {}
28
+ attribute :bought_at, :datetime, default: -> { Time.new }
29
+
30
+ validates :title, presence: true
31
+ end
32
+
33
+ ## Installation
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'options_model'
38
+ ```
39
+
40
+ And then execute:
41
+ ```bash
42
+ $ bundle
43
+ ```
44
+
45
+ ## Contributing
46
+
47
+ 1. Fork it
48
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
49
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
50
+ 4. Push to the branch (`git push origin my-new-feature`)
51
+ 5. Create new Pull Request
52
+
53
+ Please write unit test with your code if necessary.
54
+
55
+ ## License
56
+ 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,33 @@
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 = 'OptionsModel'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,13 @@
1
+ require "active_model"
2
+ require "active_model/type"
3
+
4
+ require "options_model/concerns/attribute_assignment"
5
+ require "options_model/concerns/attributes"
6
+ require "options_model/concerns/name_hacking"
7
+ require "options_model/concerns/serialization"
8
+
9
+ require "options_model/base"
10
+
11
+ module OptionsModel
12
+
13
+ end
@@ -0,0 +1,36 @@
1
+ module OptionsModel
2
+ class Base
3
+ include ActiveModel::Model
4
+ include OptionsModel::Concerns::Serialization
5
+ include OptionsModel::Concerns::Attributes
6
+ include OptionsModel::Concerns::AttributeAssignment
7
+
8
+ validate do
9
+ self.class.attribute_names.each do |attribute_name|
10
+ attribute = public_send(attribute_name)
11
+ if attribute.is_a?(self.class) && attribute.invalid?
12
+ errors.add attribute_name, :invalid
13
+ end
14
+ end
15
+ end
16
+
17
+ def inspect
18
+ "#<#{self.class.name}:OptionsModel #{self.to_h}>"
19
+ end
20
+
21
+ def self.inspect
22
+ "#<#{name}:OptionsModel [#{attribute_names.map(&:inspect).join(', ')}]>"
23
+ end
24
+
25
+ def persisted?
26
+ true
27
+ end
28
+
29
+ def self.derive(name)
30
+ Class.new(self) do
31
+ include OptionsModel::Concerns::NameHacking
32
+ self.name = name
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,77 @@
1
+ module OptionsModel::Concerns
2
+ module AttributeAssignment
3
+ extend ActiveSupport::Concern
4
+
5
+ def initialize(attributes = {})
6
+ update_attributes(attributes)
7
+ end
8
+
9
+ def initialize_dup(other)
10
+ super
11
+
12
+ update_attributes(other)
13
+ end
14
+
15
+ def update_attributes(other)
16
+ return unless other
17
+
18
+ unless other.respond_to?(:to_h)
19
+ raise ArgumentError, "#{other} must be respond to `to_h`"
20
+ end
21
+
22
+ other.to_h.each do |k, v|
23
+ if respond_to?("#{k}=")
24
+ public_send("#{k}=", v)
25
+ else
26
+ unused_attributes[k] = v
27
+ end
28
+ end
29
+ end
30
+
31
+ def [](key)
32
+ public_send(key) if respond_to?(key)
33
+ end
34
+
35
+ def []=(key, val)
36
+ setter = "#{key}="
37
+ if respond_to?(setter)
38
+ public_send(setter, val)
39
+ else
40
+ unused_attributes[key] = val
41
+ end
42
+ end
43
+
44
+ def fetch(key, default = nil)
45
+ if self.class.attribute_names.exclude?(key.to_sym) && default.nil? && !block_given?
46
+ raise KeyError, "attribute not found"
47
+ end
48
+
49
+ value = respond_to?(key) ? public_send(key) : nil
50
+ return value if value
51
+
52
+ if default
53
+ default
54
+ elsif block_given?
55
+ yield
56
+ end
57
+ end
58
+
59
+ def _attributes
60
+ @attributes ||= ActiveSupport::HashWithIndifferentAccess.new
61
+ end
62
+ private :_attributes
63
+ alias_method :attributes, :_attributes
64
+
65
+ def _nested_attributes
66
+ @nested_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
67
+ end
68
+ private :_nested_attributes
69
+ alias_method :nested_attributes, :_nested_attributes
70
+
71
+ def _unused_attributes
72
+ @unused_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
73
+ end
74
+ private :_unused_attributes
75
+ alias_method :unused_attributes, :_unused_attributes
76
+ end
77
+ end
@@ -0,0 +1,203 @@
1
+ module OptionsModel::Concerns
2
+ module Attributes
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def attribute(name, cast_type, default: nil, array: false)
7
+ check_not_finalized!
8
+
9
+ name = name.to_sym
10
+ check_name_validity! name
11
+
12
+ ActiveModel::Type.lookup(cast_type)
13
+
14
+ attribute_defaults[name] = default
15
+
16
+ generated_attribute_methods.synchronize do
17
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
18
+ def #{name}
19
+ value = attributes[:#{name}]
20
+ return value unless value.nil?
21
+ attributes[:#{name}] = self.class.attribute_defaults[:#{name}].#{default.respond_to?(:call) ? "call" : "dup"}
22
+ attributes[:#{name}]
23
+ end
24
+ STR
25
+
26
+ if array
27
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
28
+ def #{name}=(value)
29
+ if value.respond_to?(:to_a)
30
+ attributes[:#{name}] = value.to_a.map { |i| ActiveModel::Type.lookup(:#{cast_type}).cast(i) }
31
+ elsif value.nil?
32
+ attributes[:#{name}] = self.class.attribute_defaults[:#{name}].#{default.respond_to?(:call) ? "call" : "dup"}
33
+ else
34
+ raise ArgumentError,
35
+ "`value` should respond to `to_a`, but got \#{value.class} -- \#{value.inspect}"
36
+ end
37
+ end
38
+ STR
39
+ else
40
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
41
+ def #{name}=(value)
42
+ attributes[:#{name}] = ActiveModel::Type.lookup(:#{cast_type}).cast(value)
43
+ end
44
+ STR
45
+
46
+ if cast_type == :boolean
47
+ generated_attribute_methods.send :alias_method, :"#{name}?", name
48
+ end
49
+ end
50
+ end
51
+
52
+ self.attribute_names_for_inlining << name
53
+ name
54
+ end
55
+
56
+ def enum_attribute(name, enum, default: nil, allow_nil: false)
57
+ check_not_finalized!
58
+
59
+ unless enum.is_a?(Array) && enum.any?
60
+ raise ArgumentError, "enum should be an Array and can't empty"
61
+ end
62
+ enum = enum.map(&:to_s)
63
+
64
+ attribute name, :string, default: default
65
+
66
+ pluralized_name = name.to_s.pluralize
67
+ generated_class_methods.synchronize do
68
+ generated_class_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
69
+ def #{pluralized_name}
70
+ %w(#{enum.join(" ")}).freeze
71
+ end
72
+ STR
73
+
74
+ validates name, inclusion: {in: enum}, allow_nil: allow_nil
75
+ end
76
+ end
77
+
78
+ def embeds_one(name, class_name: nil, anonymous_class: nil)
79
+ check_not_finalized!
80
+
81
+ if class_name.blank? && anonymous_class.nil?
82
+ raise ArgumentError, "must provide at least one of `class_name` or `anonymous_class`"
83
+ end
84
+
85
+ name = name.to_sym
86
+ check_name_validity! name
87
+
88
+ if class_name.present?
89
+ nested_classes[name] = class_name.constantize
90
+ else
91
+ nested_classes[name] = anonymous_class
92
+ end
93
+
94
+ generated_attribute_methods.synchronize do
95
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
96
+ def #{name}
97
+ nested_attributes[:#{name}] ||= self.class.nested_classes[:#{name}].new(attributes[:#{name}])
98
+ end
99
+ STR
100
+
101
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
102
+ def #{name}=(value)
103
+ klass = self.class.nested_classes[:#{name}]
104
+ if value.respond_to?(:to_h)
105
+ nested_attributes[:#{name}] = klass.new(value.to_h)
106
+ elsif value.is_a? klass
107
+ nested_attributes[:#{name}] = value
108
+ elsif value.nil?
109
+ nested_attributes[:#{name}] = klass.new
110
+ else
111
+ raise ArgumentError,
112
+ "`value` should respond to `to_h` or \#{klass}, but got \#{value.class}"
113
+ end
114
+ end
115
+ STR
116
+ end
117
+
118
+ self.attribute_names_for_nesting << name
119
+ name
120
+ end
121
+
122
+ def attribute_defaults
123
+ @attribute_defaults ||= ActiveSupport::HashWithIndifferentAccess.new
124
+ end
125
+
126
+ def nested_classes
127
+ @nested_classes ||= ActiveSupport::HashWithIndifferentAccess.new
128
+ end
129
+
130
+ def attribute_names_for_nesting
131
+ @attribute_names_for_nesting ||= Set.new
132
+ end
133
+
134
+ def attribute_names_for_inlining
135
+ @attribute_names_for_inlining ||= Set.new
136
+ end
137
+
138
+ def attribute_names
139
+ attribute_names_for_nesting + attribute_names_for_inlining
140
+ end
141
+
142
+ def finalized?
143
+ @finalized ||= false
144
+ end
145
+
146
+ def finalize!(nested = true)
147
+ if nested
148
+ nested_classes.values.each &:finalize!
149
+ end
150
+
151
+ @finalized = true
152
+ end
153
+
154
+ protected
155
+
156
+ def check_name_validity!(symbolized_name)
157
+ if dangerous_attribute_method?(symbolized_name)
158
+ raise ArgumentError, "#{symbolized_name} is defined by #{OptionsModel::Base}. Check to make sure that you don't have an attribute or method with the same name."
159
+ end
160
+
161
+ if attribute_names_for_inlining.include?(symbolized_name) || attribute_names_for_nesting.include?(symbolized_name)
162
+ raise ArgumentError, "duplicate define attribute `#{symbolized_name}`"
163
+ end
164
+ end
165
+
166
+ def check_not_finalized!
167
+ if finalized?
168
+ raise "can't modify finalized #{self}"
169
+ end
170
+ end
171
+
172
+ # A method name is 'dangerous' if it is already (re)defined by OptionsModel, but
173
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
174
+ def dangerous_attribute_method?(name) # :nodoc:
175
+ method_defined_within?(name, OptionsModel::Base)
176
+ end
177
+
178
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
179
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
180
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
181
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
182
+ else
183
+ true
184
+ end
185
+ else
186
+ false
187
+ end
188
+ end
189
+
190
+ def generated_attribute_methods
191
+ @generated_attribute_methods ||= Module.new {
192
+ extend Mutex_m
193
+ }.tap { |mod| include mod }
194
+ end
195
+
196
+ def generated_class_methods
197
+ @generated_class_methods ||= Module.new {
198
+ extend Mutex_m
199
+ }.tap { |mod| extend mod }
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,19 @@
1
+ module OptionsModel::Concerns
2
+ module NameHacking
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def name
7
+ @_name
8
+ end
9
+
10
+ def name=(value)
11
+ unless /^[A-Z][a-zA-Z_0-9]*$/ =~ value
12
+ raise ArgumentError, "`name` must a valid class name"
13
+ end
14
+
15
+ @_name = value
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ module OptionsModel::Concerns
2
+ module Serialization
3
+ extend ActiveSupport::Concern
4
+
5
+ def to_h
6
+ hash = {}
7
+
8
+ self.class.attribute_names.each do |attribute_name|
9
+ attribute = public_send(attribute_name)
10
+ if attribute.is_a?(OptionsModel::Base)
11
+ hash[attribute_name] = attribute.to_h
12
+ else
13
+ hash[attribute_name] = attribute
14
+ end
15
+ end
16
+
17
+ hash
18
+ end
19
+
20
+ def to_h_with_unused
21
+ to_h.merge unused_attributes
22
+ end
23
+
24
+ module ClassMethods
25
+ def dump(obj)
26
+ return YAML.dump({}) unless obj
27
+
28
+ unless obj.is_a? self
29
+ raise ArgumentError,
30
+ "can't dump: was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
31
+ end
32
+
33
+ YAML.dump obj.to_h
34
+ end
35
+
36
+ def load(yaml)
37
+ return new unless yaml
38
+ return new unless yaml.is_a?(String) && /^---/.match?(yaml)
39
+
40
+ hash = YAML.load(yaml) || Hash.new
41
+
42
+ unless hash.is_a? Hash
43
+ raise ArgumentError,
44
+ "can't load: was supposed to be a #{Hash}, but was a #{hash.class}. -- #{hash.inspect}"
45
+ end
46
+
47
+ new hash
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module OptionsModel
2
+ VERSION = "0.0.1".freeze
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :options_model do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: options_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - jasl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: An ActiveModel implementation that make easier to handle model which
56
+ will be serialized in a real model.
57
+ email:
58
+ - jasl9187@hotmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/options_model.rb
67
+ - lib/options_model/base.rb
68
+ - lib/options_model/concerns/attribute_assignment.rb
69
+ - lib/options_model/concerns/attributes.rb
70
+ - lib/options_model/concerns/name_hacking.rb
71
+ - lib/options_model/concerns/serialization.rb
72
+ - lib/options_model/version.rb
73
+ - lib/tasks/options_model_tasks.rake
74
+ homepage: https://github.com/jasl-lab/options_model
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.6.12
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Make easier to handle model which will be serialized in a real model.
98
+ test_files: []