rasti-model 2.0.1 → 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/README.md +84 -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 +76 -0
- metadata +7 -6
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
|
data/README.md
CHANGED
|
@@ -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
|
@@ -290,4 +290,80 @@ describe Rasti::Model do
|
|
|
290
290
|
error.message.must_equal 'Attribute x already exists'
|
|
291
291
|
end
|
|
292
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
|
+
|
|
293
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.0
|
|
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
|
|
@@ -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:
|