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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd4831ac82eadc7515e4103a10fe010346ec8719d16db0d30103a713b5cd15ef
4
- data.tar.gz: b363b3bb323dbb5d1eec404e6adb7c3eef6d6673b0a19474d467a40551f280f8
3
+ metadata.gz: efc4252fe73eba82ba9eb35be17934864d7384a01428355706c401def3abb613
4
+ data.tar.gz: 2906e648b0c76fb766024a168a3c7cd09d2661a15283c5f3e6341ebe0f167c10
5
5
  SHA512:
6
- metadata.gz: 2e0659afca7e7f8a38731ea5901029f4f346895f357430970eba5f251745e2ceaa765ac9731e47c03a7504e2d66d9fccf57e08ea7f58230fabab8c86ca3e2962
7
- data.tar.gz: aabf1480e4bd1b45c6cde754775714047beff31d89a571fb1b617f4aa4f174a65b922fecd8ddcd90ee3b2acaedf5e3aae70a63d33ddfdcbdde41bbe35fcb8fc9
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
@@ -1,5 +1,5 @@
1
1
  module Rasti
2
2
  class Model
3
- VERSION = '2.0.1'
3
+ VERSION = '2.1.0'
4
4
  end
5
5
  end
data/lib/rasti/model.rb CHANGED
@@ -32,6 +32,10 @@ module Rasti
32
32
  name || self.superclass.name
33
33
  end
34
34
 
35
+ def to_schema
36
+ Schema.serialize self
37
+ end
38
+
35
39
  def to_s
36
40
  "#{model_name}[#{attribute_names.join(', ')}]"
37
41
  end
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.1
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: 2025-04-11 00:00:00.000000000 Z
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.6
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: