rasti-model 1.0.0

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
+ 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
@@ -0,0 +1,2 @@
1
+ service_name: travis-ci
2
+ repo_token: fDVce50x52PcuFDijSeztYNMIBd3HFcg2
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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
@@ -0,0 +1,20 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+ - 2.6
11
+ - 2.7
12
+ - jruby-9.2.9.0
13
+ - ruby-head
14
+ - jruby-head
15
+
16
+ matrix:
17
+ fast_finish: true
18
+ allow_failures:
19
+ - rvm: ruby-head
20
+ - rvm: jruby-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rasti-form.gemspec
4
+ gemspec
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
+ [![Gem Version](https://badge.fury.io/rb/rasti-model.svg)](https://rubygems.org/gems/rasti-model)
4
+ [![Build Status](https://travis-ci.org/gabynaiman/rasti-model.svg?branch=master)](https://travis-ci.org/gabynaiman/rasti-model)
5
+ [![Coverage Status](https://coveralls.io/repos/github/gabynaiman/rasti-model/badge.svg?branch=master)](https://coveralls.io/github/gabynaiman/rasti-model?branch=master)
6
+ [![Code Climate](https://codeclimate.com/github/gabynaiman/rasti-model.svg)](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
@@ -0,0 +1 @@
1
+ require_relative 'rasti/model'
@@ -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
@@ -0,0 +1,5 @@
1
+ module Rasti
2
+ class Model
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -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,5 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+
4
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
5
+ SimpleCov.start
@@ -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
@@ -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