rasti-model 2.0.0 → 2.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +26 -0
- data/README.md +85 -1
- data/lib/rasti/model/attribute.rb +4 -0
- data/lib/rasti/model/schema.rb +83 -0
- data/lib/rasti/model/version.rb +1 -1
- data/lib/rasti/model.rb +4 -0
- data/spec/model_spec.rb +86 -0
- metadata +8 -7
- data/.travis.yml +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efc4252fe73eba82ba9eb35be17934864d7384a01428355706c401def3abb613
|
|
4
|
+
data.tar.gz: 2906e648b0c76fb766024a168a3c7cd09d2661a15283c5f3e6341ebe0f167c10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08942eb37b0335bae2cc80f1cf17692cd9d736e6801d93b285be8fa34c1500d76e3129557425b858f47c449ba4e6a2e9588e3efd38793da3b0f9949a99a0a8a3'
|
|
7
|
+
data.tar.gz: 1cd16e7e5ce96a2ab0d7bd2c9ba22917451ceb90d7009e6c02d3c488d823a2712d7eaadd50597e9e4a4d81256d1b0ecbad17f872c76909ddcf41b332797060fa
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ '**' ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ '**' ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
|
|
12
|
+
name: Tests
|
|
13
|
+
runs-on: ubuntu-22.04
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
ruby-version: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', 'jruby-9.2.9.0']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v3
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: bundle exec rake
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Rasti::Model
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/rasti-model)
|
|
4
|
-
[](https://github.com/gabynaiman/rasti-model/actions/workflows/ci.yml)
|
|
5
5
|
[](https://coveralls.io/github/gabynaiman/rasti-model?branch=master)
|
|
6
6
|
[](https://codeclimate.com/github/gabynaiman/rasti-model)
|
|
7
7
|
|
|
@@ -35,6 +35,9 @@ end
|
|
|
35
35
|
point = Point.new x: 1, y: 2
|
|
36
36
|
point.x # => 1
|
|
37
37
|
point.y # => 2
|
|
38
|
+
|
|
39
|
+
# Unexpected attributes
|
|
40
|
+
Point.new z: 3 # => Rasti::Model::UnexpectedAttributesError: Unexpected attributes: z
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
### Typed models
|
|
@@ -77,6 +80,54 @@ country.name # => 'Argentina'
|
|
|
77
80
|
country.cities # => [City[name: "Buenos Aires"], City[name: "Córdoba"], City[name: "Rosario"]]
|
|
78
81
|
|
|
79
82
|
country.to_h # => attributes
|
|
83
|
+
|
|
84
|
+
# Attribute filtering
|
|
85
|
+
country.to_h(only: [:name]) # => {name: "Argentina"}
|
|
86
|
+
country.to_h(except: [:cities]) # => {name: "Argentina"}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Default values
|
|
90
|
+
```ruby
|
|
91
|
+
class User < Rasti::Model
|
|
92
|
+
attribute :name, T::String
|
|
93
|
+
attribute :admin, T::Boolean, default: false
|
|
94
|
+
attribute :created_at, T::Time, default: ->(m) { Time.now }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
user = User.new name: 'John'
|
|
98
|
+
user.admin # => false
|
|
99
|
+
user.created_at # => 2026-01-02 23:19:15 -0300
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Merging models
|
|
103
|
+
```ruby
|
|
104
|
+
point_1 = Point.new x: 1, y: 2
|
|
105
|
+
point_2 = point_1.merge x: 10
|
|
106
|
+
point_2.to_h # => {x: 10, y: 2}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Custom attribute options
|
|
110
|
+
You can add custom metadata to attributes that can be used later (e.g., for UI generation):
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
class User < Rasti::Model
|
|
114
|
+
attribute :name, T::String, description: 'The user full name'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
attribute = User.attributes.first
|
|
118
|
+
attribute.option(:description) # => 'The user full name'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
These options are also included in the schema representation.
|
|
122
|
+
|
|
123
|
+
### Equality
|
|
124
|
+
```ruby
|
|
125
|
+
point_1 = Point.new x: 1, y: 2
|
|
126
|
+
point_2 = Point.new x: 1, y: 2
|
|
127
|
+
point_3 = Point.new x: 2, y: 1
|
|
128
|
+
|
|
129
|
+
point_1 == point_2 # => true
|
|
130
|
+
point_1 == point_3 # => false
|
|
80
131
|
```
|
|
81
132
|
|
|
82
133
|
### Error handling
|
|
@@ -86,8 +137,41 @@ TypedPoint = Rasti::Model[x: T::Integer, y: T::Integer]
|
|
|
86
137
|
point = TypedPoint.new x: true
|
|
87
138
|
point.x # => Rasti::Types::CastError: Invalid cast: true -> Rasti::Types::Integer
|
|
88
139
|
point.y # => Rasti::Model::NotAssignedAttributeError: Not assigned attribute y
|
|
140
|
+
|
|
141
|
+
# Bulk validation
|
|
142
|
+
point = TypedPoint.new x: 'invalid', y: 'invalid'
|
|
143
|
+
point.cast_attributes! # => Rasti::Types::CompoundError: x: ["Invalid cast: \"invalid\" -> Rasti::Types::Integer"], y: ["Invalid cast: \"invalid\" -> Rasti::Types::Integer"]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Model Schema
|
|
147
|
+
It is possible to obtain a serializable representation of the model structure (schema).
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
Point = Rasti::Model[x: T::Integer, y: T::Integer]
|
|
151
|
+
Point.to_schema
|
|
152
|
+
# => {
|
|
153
|
+
# model: "Point",
|
|
154
|
+
# attributes: [
|
|
155
|
+
# {name: :x, type: :integer},
|
|
156
|
+
# {name: :y, type: :integer}
|
|
157
|
+
# ]
|
|
158
|
+
# }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Custom type serializers
|
|
162
|
+
You can register custom serializers for your types to be used in the schema generation:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
Rasti::Model::Schema.register_type_serializer(MyCustomType, :custom)
|
|
166
|
+
|
|
167
|
+
# Or with a block for more details
|
|
168
|
+
Rasti::Model::Schema.register_type_serializer(MyCustomType) do |type|
|
|
169
|
+
{type: :custom, details: type.some_info}
|
|
170
|
+
end
|
|
89
171
|
```
|
|
90
172
|
|
|
173
|
+
Also, if a type responds to `to_schema`, it will be used.
|
|
174
|
+
|
|
91
175
|
## Contributing
|
|
92
176
|
|
|
93
177
|
Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-model.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
class Model
|
|
3
|
+
module Schema
|
|
4
|
+
class << self
|
|
5
|
+
|
|
6
|
+
def register_type_serializer(type, serialized_type=nil, &block)
|
|
7
|
+
type_serializers[type] = block || serialized_type
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def serialize(model_class)
|
|
11
|
+
attributes = model_class.attributes.map do |attribute|
|
|
12
|
+
serialize_attribute(attribute).merge(name: attribute.name)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
model: model_class.name || model_class.to_s,
|
|
17
|
+
attributes: attributes
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def type_serializers
|
|
24
|
+
@type_serializers ||= {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def serialize_attribute(attribute)
|
|
28
|
+
serialization = serialize_type(attribute.type)
|
|
29
|
+
|
|
30
|
+
options = attribute.send(:options)
|
|
31
|
+
serialization[:options] = options unless options.empty?
|
|
32
|
+
|
|
33
|
+
serialization
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serialize_type(type)
|
|
37
|
+
if type.nil?
|
|
38
|
+
return {type: :any}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if (serializer = type_serializers[type])
|
|
42
|
+
return serializer.is_a?(Proc) ? serializer.call(type) : {type: serializer}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if type.respond_to? :to_schema
|
|
46
|
+
return type.to_schema
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if type.is_a?(Class)
|
|
50
|
+
if type <= Types::String
|
|
51
|
+
{type: :string}
|
|
52
|
+
elsif type <= Types::Integer
|
|
53
|
+
{type: :integer}
|
|
54
|
+
elsif type <= Types::Float
|
|
55
|
+
{type: :float}
|
|
56
|
+
elsif type <= Types::Boolean
|
|
57
|
+
{type: :boolean}
|
|
58
|
+
elsif type <= Types::Symbol
|
|
59
|
+
{type: :symbol}
|
|
60
|
+
elsif type <= Types::Regexp
|
|
61
|
+
{type: :regexp}
|
|
62
|
+
else
|
|
63
|
+
{type: :unknown, details: type.name || type.to_s}
|
|
64
|
+
end
|
|
65
|
+
elsif type.is_a?(Types::Time)
|
|
66
|
+
{type: :time, format: type.format}
|
|
67
|
+
elsif type.respond_to?(:values)
|
|
68
|
+
{type: :enum, values: type.values}
|
|
69
|
+
elsif type.is_a?(Types::Array)
|
|
70
|
+
{type: :array, items: serialize_type(type.type)}
|
|
71
|
+
elsif type.is_a?(Types::Hash)
|
|
72
|
+
{type: :hash, key_type: serialize_type(type.key_type), value_type: serialize_type(type.value_type)}
|
|
73
|
+
elsif type.is_a?(Types::Model)
|
|
74
|
+
{type: :model, model: type.model.name || type.model.to_s, schema: serialize(type.model)}
|
|
75
|
+
else
|
|
76
|
+
{type: :unknown, details: type.to_s}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/rasti/model/version.rb
CHANGED
data/lib/rasti/model.rb
CHANGED
data/spec/model_spec.rb
CHANGED
|
@@ -257,6 +257,16 @@ describe Rasti::Model do
|
|
|
257
257
|
point_2.y.must_equal 2
|
|
258
258
|
end
|
|
259
259
|
|
|
260
|
+
it 'Custom attribute options' do
|
|
261
|
+
model = Class.new(Rasti::Model) do
|
|
262
|
+
attribute :text, T::String, description: 'Test description'
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
attribute = model.attributes.first
|
|
266
|
+
attribute.option(:description).must_equal 'Test description'
|
|
267
|
+
attribute.option(:undefined).must_be_nil
|
|
268
|
+
end
|
|
269
|
+
|
|
260
270
|
it 'to_s' do
|
|
261
271
|
Position.to_s.must_equal 'Position[type, point]'
|
|
262
272
|
|
|
@@ -280,4 +290,80 @@ describe Rasti::Model do
|
|
|
280
290
|
error.message.must_equal 'Attribute x already exists'
|
|
281
291
|
end
|
|
282
292
|
|
|
293
|
+
describe 'Schema' do
|
|
294
|
+
|
|
295
|
+
it 'Simple' do
|
|
296
|
+
model = Rasti::Model[id: T::Integer, name: T::String]
|
|
297
|
+
|
|
298
|
+
model.to_schema.must_equal({
|
|
299
|
+
model: model.to_s,
|
|
300
|
+
attributes: [
|
|
301
|
+
{name: :id, type: :integer},
|
|
302
|
+
{name: :name, type: :string}
|
|
303
|
+
]
|
|
304
|
+
})
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'Complex' do
|
|
308
|
+
address_class = Rasti::Model[street: T::String, number: T::Integer]
|
|
309
|
+
|
|
310
|
+
model = Rasti::Model[
|
|
311
|
+
id: T::Integer,
|
|
312
|
+
tags: T::Array[T::String],
|
|
313
|
+
address: T::Model[address_class],
|
|
314
|
+
metadata: T::Hash[T::Symbol, T::String],
|
|
315
|
+
status: T::Enum['active', 'inactive'],
|
|
316
|
+
created_at: T::Time['%Y-%m-%d']
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
model.to_schema.must_equal({
|
|
320
|
+
model: model.to_s,
|
|
321
|
+
attributes: [
|
|
322
|
+
{name: :id, type: :integer},
|
|
323
|
+
{name: :tags, type: :array, items: {type: :string}},
|
|
324
|
+
{name: :address, type: :model, model: address_class.to_s, schema: {
|
|
325
|
+
model: address_class.to_s,
|
|
326
|
+
attributes: [
|
|
327
|
+
{name: :street, type: :string},
|
|
328
|
+
{name: :number, type: :integer}
|
|
329
|
+
]
|
|
330
|
+
}},
|
|
331
|
+
{name: :metadata, type: :hash, key_type: {type: :symbol}, value_type: {type: :string}},
|
|
332
|
+
{name: :status, type: :enum, values: ['active', 'inactive']},
|
|
333
|
+
{name: :created_at, type: :time, format: '%Y-%m-%d'}
|
|
334
|
+
]
|
|
335
|
+
})
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it 'With options' do
|
|
339
|
+
model = Class.new(Rasti::Model) do
|
|
340
|
+
attribute :text, T::String, description: 'Basic text'
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
model.to_schema.must_equal({
|
|
344
|
+
model: model.to_s,
|
|
345
|
+
attributes: [
|
|
346
|
+
{name: :text, type: :string, options: {description: 'Basic text'}}
|
|
347
|
+
]
|
|
348
|
+
})
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'Type with to_schema' do
|
|
352
|
+
custom_type = Object.new
|
|
353
|
+
custom_type.define_singleton_method :to_schema do
|
|
354
|
+
{type: :custom, details: 'custom details'}
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
model = Rasti::Model[attr: custom_type]
|
|
358
|
+
|
|
359
|
+
model.to_schema.must_equal({
|
|
360
|
+
model: model.to_s,
|
|
361
|
+
attributes: [
|
|
362
|
+
{name: :attr, type: :custom, details: 'custom details'}
|
|
363
|
+
]
|
|
364
|
+
})
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
end
|
|
368
|
+
|
|
283
369
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rasti-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Naiman
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: multi_require
|
|
@@ -150,10 +150,10 @@ extensions: []
|
|
|
150
150
|
extra_rdoc_files: []
|
|
151
151
|
files:
|
|
152
152
|
- ".coveralls.yml"
|
|
153
|
+
- ".github/workflows/ci.yml"
|
|
153
154
|
- ".gitignore"
|
|
154
155
|
- ".ruby-gemset"
|
|
155
156
|
- ".ruby-version"
|
|
156
|
-
- ".travis.yml"
|
|
157
157
|
- Gemfile
|
|
158
158
|
- LICENSE.txt
|
|
159
159
|
- README.md
|
|
@@ -162,6 +162,7 @@ files:
|
|
|
162
162
|
- lib/rasti/model.rb
|
|
163
163
|
- lib/rasti/model/attribute.rb
|
|
164
164
|
- lib/rasti/model/errors.rb
|
|
165
|
+
- lib/rasti/model/schema.rb
|
|
165
166
|
- lib/rasti/model/version.rb
|
|
166
167
|
- rasti-model.gemspec
|
|
167
168
|
- spec/coverage_helper.rb
|
|
@@ -171,7 +172,7 @@ homepage: https://github.com/gabynaiman/rasti-model
|
|
|
171
172
|
licenses:
|
|
172
173
|
- MIT
|
|
173
174
|
metadata: {}
|
|
174
|
-
post_install_message:
|
|
175
|
+
post_install_message:
|
|
175
176
|
rdoc_options: []
|
|
176
177
|
require_paths:
|
|
177
178
|
- lib
|
|
@@ -186,8 +187,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
186
187
|
- !ruby/object:Gem::Version
|
|
187
188
|
version: '0'
|
|
188
189
|
requirements: []
|
|
189
|
-
rubygems_version: 3.0.
|
|
190
|
-
signing_key:
|
|
190
|
+
rubygems_version: 3.0.9
|
|
191
|
+
signing_key:
|
|
191
192
|
specification_version: 4
|
|
192
193
|
summary: Domain models with typed attributes
|
|
193
194
|
test_files:
|
data/.travis.yml
DELETED