rasti-model 1.0.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 +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +25 -0
- data/lib/rasti-model.rb +1 -0
- data/lib/rasti/model.rb +200 -0
- data/lib/rasti/model/attribute.rb +32 -0
- data/lib/rasti/model/errors.rb +27 -0
- data/lib/rasti/model/version.rb +5 -0
- data/rasti-model.gemspec +31 -0
- data/spec/coverage_helper.rb +5 -0
- data/spec/minitest_helper.rb +22 -0
- data/spec/model_spec.rb +277 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 69fc7d6502deb7e66837ef4fdd561b1ac56b9125c3420612e0574b28122dcac0
|
4
|
+
data.tar.gz: 83a974c72689d66b70ed7cbe7d50e0dc0ae93ec4acabc471333124b21d3d0d2e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e9dc23d735dee6c67b37a789cd2146b65dcfd2eaa3bc811cd937d7e80d3361bca87760eb2a49a23d3b94941386eab1cc34e4fdffc06fdd9deba4b86730b93af1
|
7
|
+
data.tar.gz: dd9c2d798a13065f2825a9b3a988b6c2e7de4333e1a198a052fe24848d2dd96c54e55c187ecd7da2b60dc181adc81f2fbc97434af6f61cb2a5e0a1d2c1e07a84
|
data/.coveralls.yml
ADDED
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rasti-model
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.5.7
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Gabriel Naiman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Rasti::Model
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/rasti-model)
|
4
|
+
[](https://travis-ci.org/gabynaiman/rasti-model)
|
5
|
+
[](https://coveralls.io/github/gabynaiman/rasti-model?branch=master)
|
6
|
+
[](https://codeclimate.com/github/gabynaiman/rasti-model)
|
7
|
+
|
8
|
+
Domain models with typed attributes
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'rasti-model'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install rasti-model
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
### Basic models
|
29
|
+
```ruby
|
30
|
+
class Point < Rasti::Model
|
31
|
+
attribute :x
|
32
|
+
attribute :y
|
33
|
+
end
|
34
|
+
|
35
|
+
point = Point.new x: 1, y: 2
|
36
|
+
point.x # => 1
|
37
|
+
point.y # => 2
|
38
|
+
```
|
39
|
+
|
40
|
+
### Typed models
|
41
|
+
```ruby
|
42
|
+
T = Rasti::Types
|
43
|
+
|
44
|
+
class TypedPoint < Rasti::Model
|
45
|
+
attribute :x, T::Integer
|
46
|
+
attribute :y, T::Integer
|
47
|
+
end
|
48
|
+
|
49
|
+
point = TypedPoint.new x: '1', y: '2'
|
50
|
+
point.x # => 1
|
51
|
+
point.y # => 2
|
52
|
+
```
|
53
|
+
|
54
|
+
### Inline definition
|
55
|
+
```ruby
|
56
|
+
Point = Rasti::Model[:x, :y]
|
57
|
+
|
58
|
+
TypedPoint = Rasti::Model[x: T::Integer, y: T::Integer]
|
59
|
+
```
|
60
|
+
|
61
|
+
### Serialization and deserialization
|
62
|
+
```ruby
|
63
|
+
City = Rasti::Model[name: T::String]
|
64
|
+
Country = Rasti::Model[name: T::String, cities: T::Array[T::Model[City]]]
|
65
|
+
|
66
|
+
attributes = {
|
67
|
+
name: 'Argentina',
|
68
|
+
cities: [
|
69
|
+
{name: 'Buenos Aires'},
|
70
|
+
{name: 'Córdoba'},
|
71
|
+
{name: 'Rosario'}
|
72
|
+
]
|
73
|
+
}
|
74
|
+
|
75
|
+
country = Country.new attributes
|
76
|
+
country.name # => 'Argentina'
|
77
|
+
country.cities # => [City[name: "Buenos Aires"], City[name: "Córdoba"], City[name: "Rosario"]]
|
78
|
+
|
79
|
+
country.to_h # => attributes
|
80
|
+
```
|
81
|
+
|
82
|
+
### Error handling
|
83
|
+
```ruby
|
84
|
+
TypedPoint = Rasti::Model[x: T::Integer, y: T::Integer]
|
85
|
+
|
86
|
+
point = TypedPoint.new x: true
|
87
|
+
point.x # => Rasti::Types::CastError: Invalid cast: true -> Rasti::Types::Integer
|
88
|
+
point.y # => Rasti::Model::NotAssignedAttributeError: Not assigned attribute y
|
89
|
+
```
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-model.
|
94
|
+
|
95
|
+
|
96
|
+
## License
|
97
|
+
|
98
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
99
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:spec) do |t|
|
5
|
+
t.libs << 'spec'
|
6
|
+
t.libs << 'lib'
|
7
|
+
t.pattern = ENV['DIR'] ? File.join(ENV['DIR'], '**', '*_spec.rb') : 'spec/**/*_spec.rb'
|
8
|
+
t.verbose = false
|
9
|
+
t.warning = false
|
10
|
+
t.loader = nil if ENV['TEST']
|
11
|
+
ENV['TEST'], ENV['LINE'] = ENV['TEST'].split(':') if ENV['TEST'] && !ENV['LINE']
|
12
|
+
t.options = ''
|
13
|
+
t.options << "--name=/#{ENV['NAME']}/ " if ENV['NAME']
|
14
|
+
t.options << "-l #{ENV['LINE']} " if ENV['LINE'] && ENV['TEST']
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: :spec
|
18
|
+
|
19
|
+
desc 'Pry console'
|
20
|
+
task :console do
|
21
|
+
require 'rasti-model'
|
22
|
+
require 'pry'
|
23
|
+
ARGV.clear
|
24
|
+
Pry.start
|
25
|
+
end
|
data/lib/rasti-model.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'rasti/model'
|
data/lib/rasti/model.rb
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'multi_require'
|
2
|
+
require 'rasti-types'
|
3
|
+
|
4
|
+
module Rasti
|
5
|
+
class Model
|
6
|
+
|
7
|
+
extend MultiRequire
|
8
|
+
|
9
|
+
require_relative_pattern 'model/*'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def [](*args)
|
14
|
+
Class.new(self) do
|
15
|
+
if args.count == 1 && args.first.is_a?(Hash)
|
16
|
+
args.first.each { |name, type| attribute name, type }
|
17
|
+
else
|
18
|
+
args.each { |name| attribute name }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def attributes
|
24
|
+
@attributes ||= []
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute_names
|
28
|
+
@attibute_names ||= attributes.map(&:name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def model_name
|
32
|
+
name || self.superclass.name
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"#{model_name}[#{attribute_names.join(', ')}]"
|
37
|
+
end
|
38
|
+
alias_method :inspect, :to_s
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def attribute(name, type=nil, options={})
|
43
|
+
raise ArgumentError, "Attribute #{name} already exists" if attributes.any? { |a| a.name == name }
|
44
|
+
attribute = Attribute.new(name, type, options)
|
45
|
+
attributes << attribute
|
46
|
+
|
47
|
+
define_method name do
|
48
|
+
read_attribute attribute
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def inherited(subclass)
|
53
|
+
subclass.instance_variable_set :@attributes, attributes.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(attributes={})
|
59
|
+
@__attributes__ = attributes
|
60
|
+
validate_defined_attributes! attributes.keys.map(&:to_sym)
|
61
|
+
end
|
62
|
+
|
63
|
+
def merge(new_attributes)
|
64
|
+
self.class.new __attributes__.merge(new_attributes)
|
65
|
+
end
|
66
|
+
|
67
|
+
def cast_attributes!
|
68
|
+
errors = {}
|
69
|
+
|
70
|
+
self.class.attributes.each do |attribute|
|
71
|
+
begin
|
72
|
+
if assigned_attribute?(attribute.name)
|
73
|
+
value = read_attribute attribute
|
74
|
+
value.cast_attributes! if value.is_a? Model
|
75
|
+
end
|
76
|
+
|
77
|
+
rescue Rasti::Types::CompoundError => ex
|
78
|
+
ex.errors.each do |key, messages|
|
79
|
+
errors["#{attribute.name}.#{key}"] = messages
|
80
|
+
end
|
81
|
+
|
82
|
+
rescue Rasti::Types::CastError => ex
|
83
|
+
errors[attribute.name] = [ex.message]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
raise Rasti::Types::CompoundError.new(errors) unless errors.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_h(options={})
|
91
|
+
if options.empty?
|
92
|
+
serialized_attributes
|
93
|
+
else
|
94
|
+
attributes_filter = {only: serialized_attributes.keys, except: []}.merge(options)
|
95
|
+
(attributes_filter[:only] - attributes_filter[:except]).each_with_object({}) do |name, hash|
|
96
|
+
hash[name] = serialized_attributes[name]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
cast_attributes!
|
104
|
+
|
105
|
+
"#{self.class.model_name}[#{__cache__.map { |n,v| "#{n}: #{v.inspect}" }.join(', ')}]"
|
106
|
+
end
|
107
|
+
alias_method :inspect, :to_s
|
108
|
+
|
109
|
+
def eql?(other)
|
110
|
+
instance_of?(other.class) && to_h.eql?(other.to_h)
|
111
|
+
end
|
112
|
+
|
113
|
+
def ==(other)
|
114
|
+
other.kind_of?(self.class) && to_h == other.to_h
|
115
|
+
end
|
116
|
+
|
117
|
+
def hash
|
118
|
+
[self.class, to_h].hash
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def __attributes__
|
124
|
+
@__attributes__ ||= {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def __cache__
|
128
|
+
@__cache__ ||= {}
|
129
|
+
end
|
130
|
+
|
131
|
+
def validate_defined_attributes!(attribute_names)
|
132
|
+
invalid_attributes = attribute_names - self.class.attribute_names
|
133
|
+
raise UnexpectedAttributesError, invalid_attributes unless invalid_attributes.empty?
|
134
|
+
end
|
135
|
+
|
136
|
+
def read_attribute(attribute)
|
137
|
+
__cache__[attribute.name] ||= begin
|
138
|
+
attribute_key = attribute_key_for attribute.name
|
139
|
+
if attribute_key
|
140
|
+
cast_attribute attribute.type, __attributes__[attribute_key]
|
141
|
+
elsif attribute.default?
|
142
|
+
value = attribute.default_value.respond_to?(:call) ? attribute.default_value.call(self) : attribute.default_value
|
143
|
+
cast_attribute attribute.type, value
|
144
|
+
else
|
145
|
+
raise NotAssignedAttributeError, attribute.name
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def cast_attribute(type, value)
|
151
|
+
if type.nil? || value.nil?
|
152
|
+
value
|
153
|
+
elsif type.is_a?(Symbol)
|
154
|
+
send type, value
|
155
|
+
else
|
156
|
+
type.cast value
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def assigned_attribute?(attr_name)
|
161
|
+
!attribute_key_for(attr_name).nil? || self.class.attributes.any? { |a| a.name == attr_name && a.default? }
|
162
|
+
end
|
163
|
+
|
164
|
+
def attribute_key_for(attr_name)
|
165
|
+
if __attributes__.key?(attr_name)
|
166
|
+
attr_name
|
167
|
+
elsif __attributes__.key?(attr_name.to_s)
|
168
|
+
attr_name.to_s
|
169
|
+
else
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def serialized_attributes
|
175
|
+
@serialized_attributes ||= begin
|
176
|
+
cast_attributes!
|
177
|
+
|
178
|
+
__cache__.each_with_object({}) do |(attr_name, value), hash|
|
179
|
+
hash[attr_name] = serialize_value value
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def serialize_value(value)
|
185
|
+
case value
|
186
|
+
when Model
|
187
|
+
value.to_h
|
188
|
+
when Array
|
189
|
+
value.map { |v| serialize_value v }
|
190
|
+
when Hash
|
191
|
+
value.each_with_object({}) do |(k,v), h|
|
192
|
+
h[k.to_sym] = serialize_value v
|
193
|
+
end
|
194
|
+
else
|
195
|
+
value
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rasti
|
2
|
+
class Model
|
3
|
+
class Attribute
|
4
|
+
|
5
|
+
attr_reader :name, :type
|
6
|
+
|
7
|
+
def initialize(name, type, options={})
|
8
|
+
@name = name
|
9
|
+
@type = type
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def default?
|
14
|
+
options.key? :default
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_value
|
18
|
+
options.fetch(:default)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"#{self.class}[name: #{name.inspect}, type: #{type.inspect}, options: #{options.inspect}]"
|
23
|
+
end
|
24
|
+
alias_method :inspect, :to_s
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :options
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rasti
|
2
|
+
class Model
|
3
|
+
|
4
|
+
class NotAssignedAttributeError < StandardError
|
5
|
+
|
6
|
+
attr_reader :attribute
|
7
|
+
|
8
|
+
def initialize(attribute)
|
9
|
+
@attribute = attribute
|
10
|
+
super "Not assigned attribute: #{attribute}"
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class UnexpectedAttributesError < StandardError
|
16
|
+
|
17
|
+
attr_reader :attributes
|
18
|
+
|
19
|
+
def initialize(attributes)
|
20
|
+
@attributes = attributes
|
21
|
+
super "Unexpected attributes: #{attributes.join(', ')}"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/rasti-model.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rasti/model/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rasti-model'
|
8
|
+
spec.version = Rasti::Model::VERSION
|
9
|
+
spec.authors = ['Gabriel Naiman']
|
10
|
+
spec.email = ['gabynaiman@gmail.com']
|
11
|
+
spec.summary = 'Domain models with typed attributes'
|
12
|
+
spec.description = 'Domain models with typed attributes'
|
13
|
+
spec.homepage = 'https://github.com/gabynaiman/rasti-model'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'multi_require', '~> 1.0'
|
22
|
+
spec.add_runtime_dependency 'rasti-types', '~> 1.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
|
26
|
+
spec.add_development_dependency 'minitest-colorin', '~> 0.1'
|
27
|
+
spec.add_development_dependency 'minitest-line', '~> 0.6'
|
28
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
29
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
30
|
+
spec.add_development_dependency 'pry-nav', '~> 0.2'
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'coverage_helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'minitest/colorin'
|
4
|
+
require 'pry-nav'
|
5
|
+
require 'rasti-model'
|
6
|
+
|
7
|
+
T = Rasti::Types
|
8
|
+
|
9
|
+
Point = Rasti::Model[:x, :y]
|
10
|
+
|
11
|
+
Point3D = Point[:z]
|
12
|
+
|
13
|
+
class Position < Rasti::Model
|
14
|
+
attribute :type, T::Enum['2D', '3D'], default: '2D'
|
15
|
+
attribute :point, :cast_point
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def cast_point(value)
|
20
|
+
type == '2D' ? Point.new(value) : Point3D.new(value)
|
21
|
+
end
|
22
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe Rasti::Model do
|
4
|
+
|
5
|
+
describe 'Initialization' do
|
6
|
+
|
7
|
+
it 'All attributes' do
|
8
|
+
point = Point.new x: 1, y: 2
|
9
|
+
point.x.must_equal 1
|
10
|
+
point.y.must_equal 2
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'Some attributes' do
|
14
|
+
point = Point.new x: 1
|
15
|
+
|
16
|
+
point.x.must_equal 1
|
17
|
+
|
18
|
+
error = proc { point.y }.must_raise Rasti::Model::NotAssignedAttributeError
|
19
|
+
error.message.must_equal 'Not assigned attribute: y'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'Unexpected attributes' do
|
23
|
+
error = proc { Point.new z: 3 }.must_raise Rasti::Model::UnexpectedAttributesError
|
24
|
+
error.message.must_equal 'Unexpected attributes: z'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'Indifferent attribute keys' do
|
28
|
+
point = Point.new 'x' => 1, 'y' => 2
|
29
|
+
point.x.must_equal 1
|
30
|
+
point.y.must_equal 2
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'Casting' do
|
36
|
+
|
37
|
+
it 'Attribute' do
|
38
|
+
model = Rasti::Model[text: T::String]
|
39
|
+
m = model.new text: 123
|
40
|
+
m.text.must_equal '123'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'Nested model' do
|
44
|
+
range = Rasti::Model[min: T::Integer, max: T::Integer]
|
45
|
+
model = Rasti::Model[range: T::Model[range]]
|
46
|
+
|
47
|
+
m = model.new range: {min: '1', max: '10'}
|
48
|
+
|
49
|
+
m.range.must_be_instance_of range
|
50
|
+
m.range.min.must_equal 1
|
51
|
+
m.range.max.must_equal 10
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'Custom' do
|
55
|
+
position_1 = Position.new type: '2D', point: {x: 1, y: 2}
|
56
|
+
|
57
|
+
position_1.point.must_be_instance_of Point
|
58
|
+
position_1.point.x.must_equal 1
|
59
|
+
position_1.point.y.must_equal 2
|
60
|
+
|
61
|
+
position_2 = Position.new type: '3D', point: {x: 1, y: 2, z: 3}
|
62
|
+
|
63
|
+
position_2.point.must_be_instance_of Point3D
|
64
|
+
position_2.point.x.must_equal 1
|
65
|
+
position_2.point.y.must_equal 2
|
66
|
+
position_2.point.z.must_equal 3
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'Invalid value' do
|
70
|
+
model = Rasti::Model[boolean: T::Boolean]
|
71
|
+
|
72
|
+
m = model.new boolean: 'x'
|
73
|
+
|
74
|
+
error = proc { m.boolean }.must_raise Rasti::Types::CastError
|
75
|
+
error.message.must_equal "Invalid cast: 'x' -> Rasti::Types::Boolean"
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'Invalid nested value' do
|
79
|
+
range = Rasti::Model[min: T::Integer, max: T::Integer]
|
80
|
+
model = Rasti::Model[range: T::Model[range]]
|
81
|
+
|
82
|
+
m = model.new range: {min: 1, max: true}
|
83
|
+
|
84
|
+
error = proc { m.range.max }.must_raise Rasti::Types::CastError
|
85
|
+
error.message.must_equal "Invalid cast: true -> Rasti::Types::Integer"
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'Defaults' do
|
91
|
+
|
92
|
+
it 'Value' do
|
93
|
+
model = Class.new(Rasti::Model) do
|
94
|
+
attribute :text, T::String, default: 'xyz'
|
95
|
+
end
|
96
|
+
|
97
|
+
m = model.new
|
98
|
+
m.text.must_equal 'xyz'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'Block' do
|
102
|
+
model = Class.new(Rasti::Model) do
|
103
|
+
attribute :time_1, T::Time['%F']
|
104
|
+
attribute :time_2, T::Time['%F'], default: ->(m) { m.time_1 }
|
105
|
+
end
|
106
|
+
|
107
|
+
m = model.new time_1: Time.now
|
108
|
+
m.time_2.must_equal m.time_1
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'Comparable' do
|
114
|
+
|
115
|
+
it 'Equivalency (==)' do
|
116
|
+
point_1 = Point.new x: 1, y: 2
|
117
|
+
point_2 = Point3D.new x: 1, y: 2
|
118
|
+
point_3 = Point.new x: 2, y: 1
|
119
|
+
|
120
|
+
assert point_1 == point_2
|
121
|
+
refute point_1 == point_3
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'Equality (eql?)' do
|
125
|
+
point_1 = Point.new x: 1, y: 2
|
126
|
+
point_2 = Point.new x: 1, y: 2
|
127
|
+
point_3 = Point3D.new x: 1, y: 2
|
128
|
+
point_4 = Point.new x: 2, y: 1
|
129
|
+
|
130
|
+
assert point_1.eql?(point_2)
|
131
|
+
refute point_1.eql?(point_3)
|
132
|
+
refute point_1.eql?(point_4)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'hash' do
|
136
|
+
point_1 = Point.new x: 1, y: 2
|
137
|
+
point_2 = Point.new x: 1, y: 2
|
138
|
+
point_3 = Point3D.new x: 1, y: 2
|
139
|
+
point_4 = Point.new x: 2, y: 1
|
140
|
+
|
141
|
+
point_1.hash.must_equal point_2.hash
|
142
|
+
point_1.hash.wont_equal point_3.hash
|
143
|
+
point_1.hash.wont_equal point_4.hash
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'Serialization and deserialization' do
|
149
|
+
|
150
|
+
let :address_class do
|
151
|
+
Rasti::Model[
|
152
|
+
street: T::String,
|
153
|
+
number: T::Integer
|
154
|
+
]
|
155
|
+
end
|
156
|
+
|
157
|
+
let :birthday_class do
|
158
|
+
Rasti::Model[
|
159
|
+
day: T::Integer,
|
160
|
+
month: T::Integer,
|
161
|
+
year: T::Integer
|
162
|
+
]
|
163
|
+
end
|
164
|
+
|
165
|
+
let :contact_class do
|
166
|
+
Rasti::Model[
|
167
|
+
id: T::Integer,
|
168
|
+
name: T::String,
|
169
|
+
birthday: T::Model[birthday_class],
|
170
|
+
phones: T::Hash[T::Symbol, T::Integer],
|
171
|
+
addresses: T::Array[T::Model[address_class]],
|
172
|
+
labels: T::Array[T::String]
|
173
|
+
]
|
174
|
+
end
|
175
|
+
|
176
|
+
let :attributes do
|
177
|
+
{
|
178
|
+
id: 12345,
|
179
|
+
name: 'John',
|
180
|
+
birthday: {
|
181
|
+
day: 19,
|
182
|
+
month: 6,
|
183
|
+
year: 1993
|
184
|
+
},
|
185
|
+
phones: {
|
186
|
+
office: 1234567890,
|
187
|
+
house: 456456456
|
188
|
+
},
|
189
|
+
addresses: [
|
190
|
+
{street: 'Lexington Avenue', number: 123},
|
191
|
+
{street: 'Park Avenue', number: 456}
|
192
|
+
],
|
193
|
+
labels: ['Friend', 'Work']
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'All' do
|
198
|
+
contact = contact_class.new attributes
|
199
|
+
contact.to_h.must_equal attributes
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'Only' do
|
203
|
+
contact = contact_class.new attributes
|
204
|
+
|
205
|
+
contact.to_h(only: [:name, :birthday]).must_equal name: attributes[:name],
|
206
|
+
birthday: attributes[:birthday]
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'Except' do
|
210
|
+
contact = contact_class.new attributes
|
211
|
+
|
212
|
+
contact.to_h(except: [:age, :addresses]).must_equal id: attributes[:id],
|
213
|
+
name: attributes[:name],
|
214
|
+
birthday: attributes[:birthday],
|
215
|
+
phones: attributes[:phones],
|
216
|
+
labels: attributes[:labels]
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'Ignore not assigned attributes' do
|
220
|
+
contact = contact_class.new birthday: {year: 1993, month: 06, day: 19}
|
221
|
+
|
222
|
+
contact.to_h.must_equal birthday: attributes[:birthday]
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'Invalid cast' do
|
226
|
+
contact = contact_class.new id: 'abcd', birthday: {year: 1993, month: 6, day: 'XIX'}
|
227
|
+
|
228
|
+
error = proc { contact.to_h }.must_raise Rasti::Types::CompoundError
|
229
|
+
error.errors.must_equal id: ["Invalid cast: 'abcd' -> Rasti::Types::Integer"],
|
230
|
+
'birthday.day' => ["Invalid cast: 'XIX' -> Rasti::Types::Integer"]
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'With defaults' do
|
234
|
+
model = Class.new(Rasti::Model) do
|
235
|
+
attribute :text, T::String, default: 'xyz'
|
236
|
+
end
|
237
|
+
|
238
|
+
model.new.to_h.must_equal text: 'xyz'
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'Merge' do
|
244
|
+
point_1 = Point.new x: 1, y: 2
|
245
|
+
point_2 = point_1.merge x: 10
|
246
|
+
|
247
|
+
point_1.x.must_equal 1
|
248
|
+
point_1.y.must_equal 2
|
249
|
+
|
250
|
+
point_2.x.must_equal 10
|
251
|
+
point_2.y.must_equal 2
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'to_s' do
|
255
|
+
Position.to_s.must_equal 'Position[type, point]'
|
256
|
+
|
257
|
+
Position.new(point: {x: 1, y: 2}).to_s.must_equal 'Position[type: "2D", point: Point[x: 1, y: 2]]'
|
258
|
+
|
259
|
+
Position.attributes.map(&:to_s).must_equal [
|
260
|
+
'Rasti::Model::Attribute[name: :type, type: Rasti::Types::Enum["2D", "3D"], options: {:default=>"2D"}]',
|
261
|
+
'Rasti::Model::Attribute[name: :point, type: :cast_point, options: {}]'
|
262
|
+
]
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'Ihnerits superclass attributes' do
|
266
|
+
point = Point3D.new x: 1, y: 2, z: 3
|
267
|
+
point.x.must_equal 1
|
268
|
+
point.y.must_equal 2
|
269
|
+
point.z.must_equal 3
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'Invalid attribute redefinition' do
|
273
|
+
error = proc { Point[x: T::String] }.must_raise ArgumentError
|
274
|
+
error.message.must_equal 'Attribute x already exists'
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
metadata
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rasti-model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gabriel Naiman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: multi_require
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rasti-types
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '12.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '12.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
- - "<"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '5.11'
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - "~>"
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '5.0'
|
72
|
+
- - "<"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '5.11'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: minitest-colorin
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.1'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.1'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: minitest-line
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.6'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.6'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: simplecov
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.12'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.12'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: coveralls
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0.8'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0.8'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: pry-nav
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.2'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0.2'
|
145
|
+
description: Domain models with typed attributes
|
146
|
+
email:
|
147
|
+
- gabynaiman@gmail.com
|
148
|
+
executables: []
|
149
|
+
extensions: []
|
150
|
+
extra_rdoc_files: []
|
151
|
+
files:
|
152
|
+
- ".coveralls.yml"
|
153
|
+
- ".gitignore"
|
154
|
+
- ".ruby-gemset"
|
155
|
+
- ".ruby-version"
|
156
|
+
- ".travis.yml"
|
157
|
+
- Gemfile
|
158
|
+
- LICENSE.txt
|
159
|
+
- README.md
|
160
|
+
- Rakefile
|
161
|
+
- lib/rasti-model.rb
|
162
|
+
- lib/rasti/model.rb
|
163
|
+
- lib/rasti/model/attribute.rb
|
164
|
+
- lib/rasti/model/errors.rb
|
165
|
+
- lib/rasti/model/version.rb
|
166
|
+
- rasti-model.gemspec
|
167
|
+
- spec/coverage_helper.rb
|
168
|
+
- spec/minitest_helper.rb
|
169
|
+
- spec/model_spec.rb
|
170
|
+
homepage: https://github.com/gabynaiman/rasti-model
|
171
|
+
licenses:
|
172
|
+
- MIT
|
173
|
+
metadata: {}
|
174
|
+
post_install_message:
|
175
|
+
rdoc_options: []
|
176
|
+
require_paths:
|
177
|
+
- lib
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
requirements: []
|
189
|
+
rubygems_version: 3.0.6
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: Domain models with typed attributes
|
193
|
+
test_files:
|
194
|
+
- spec/coverage_helper.rb
|
195
|
+
- spec/minitest_helper.rb
|
196
|
+
- spec/model_spec.rb
|