hamachi 0.1.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +66 -8
- data/lib/hamachi/ext.rb +3 -0
- data/lib/hamachi/model.rb +2 -283
- data/lib/hamachi/source/enumerable_ext.rb +36 -0
- data/lib/hamachi/source/field.rb +88 -0
- data/lib/hamachi/source/model.rb +114 -0
- data/lib/hamachi/version.rb +28 -1
- data/lib/hamachi.rb +3 -7
- metadata +11 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7664640e09c9177c316943760e26eb565dd556a22c6ebfb01708e95e57724619
|
4
|
+
data.tar.gz: 17596dac61e26f9d173a129571f999b81ff395faea800a3c9b33a668536820c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a7424287ab31ad6574dccf647825014d602268ecb5d5d016202782d2c31bac26ef48b3ed4f6cd98ab53473176bf19a2b2aa842ca2dcd8cc05529c9bd5188dea
|
7
|
+
data.tar.gz: '095213ba6c92bedc55b4e97fafa84b1ec1ffad30b3d5990f961f4a28894792ed260590651bb8fee704b3c1250f63105e4a22525b67b8951cd6c2bd7cfeb94913'
|
data/README.md
CHANGED
@@ -43,7 +43,7 @@ user = User.from_json('{"name": "Alice", "age": 30}')
|
|
43
43
|
user = User.new(name: "Alice", age: 30)
|
44
44
|
|
45
45
|
user.name = 'Bob'
|
46
|
-
user.age =
|
46
|
+
user.age = 120 # => raises RuntimeError: expected age to be 1..100, got 120
|
47
47
|
```
|
48
48
|
|
49
49
|
You can define the following types of fields:
|
@@ -59,23 +59,81 @@ You can define the following types of fields:
|
|
59
59
|
More complex nested models can be created:
|
60
60
|
|
61
61
|
```ruby
|
62
|
-
class User < Hamachi::Model
|
63
|
-
field :name, type: String
|
64
|
-
field :friends, type: list(User)
|
65
|
-
field :posts, type: list(Post)
|
66
|
-
end
|
67
|
-
|
68
62
|
class Post < Hamachi::Model
|
69
63
|
field :title, type: String
|
70
64
|
field :content, type: String
|
71
65
|
field :created_at, type: Timestamp
|
72
66
|
field :tags, type: list(String)
|
73
67
|
end
|
68
|
+
|
69
|
+
class User < Hamachi::Model
|
70
|
+
field :name, type: String
|
71
|
+
field :friends, type: list(User)
|
72
|
+
field :posts, type: list(Post)
|
73
|
+
end
|
74
74
|
```
|
75
75
|
|
76
|
+
## Notes
|
77
|
+
|
78
|
+
A model has type-checked fields.
|
79
|
+
|
80
|
+
This class can be used to create a flexible and type-safe representation of
|
81
|
+
JSON data. It provides a convenient way to create and validate data models
|
82
|
+
in Ruby, making it easier to build complex applications.
|
83
|
+
|
84
|
+
The Model class extends the built-in Hash class and is designed to enforce
|
85
|
+
type constraints on data objects that can be created from JSON snapshots. It
|
86
|
+
defines custom syntax for declaring and validating fields, with support for
|
87
|
+
common data types suchs enums, lists, and nullable types.
|
88
|
+
|
89
|
+
Example usage
|
90
|
+
|
91
|
+
class Person < Model
|
92
|
+
field %{name}, type: String
|
93
|
+
field %{gender}, type: (enum :male, :female)
|
94
|
+
field %{age}, type: 1..100
|
95
|
+
end
|
96
|
+
|
97
|
+
anna = Person.new(
|
98
|
+
name: 'Anna',
|
99
|
+
gender: :female,
|
100
|
+
age: 29,
|
101
|
+
)
|
102
|
+
|
103
|
+
Type checking in the Model framework is based on a combination of built-in
|
104
|
+
Ruby functionality and custom matchers that are optimized for working with
|
105
|
+
complex data structures.
|
106
|
+
|
107
|
+
- The framework relies on the === operator, which is a built-in method in
|
108
|
+
Ruby that checks whether a given value is a member of a class or matches
|
109
|
+
a pattern, such as a regular-expression or a range of numbers
|
110
|
+
- In addition the framework provides a set of custom matchers that are
|
111
|
+
optimized for working with more complex data structures. These matchers
|
112
|
+
include support for lists, nullable types, enumerations, and more.
|
113
|
+
|
114
|
+
Another way to extend the type checking capabilities is by subclassing the
|
115
|
+
Matcher class. This allows developers to create custom matchers that can
|
116
|
+
validate complex data structures or enforce domain-specific rules on the
|
117
|
+
values of fields in a model. This provides a powerful extension point that
|
118
|
+
allows developers to meet the needs of their specific use cases, and can
|
119
|
+
help ensure data quality and consistency in their applications.
|
120
|
+
|
121
|
+
Customizing serialization is an important aspect of working with data models,
|
122
|
+
and the Model framework provides a flexible way to achieve this through the
|
123
|
+
to_json and from_snapshot methods. These methods allow developers to control
|
124
|
+
how data is represented in JSON format, which can be important ensure that
|
125
|
+
the serialized data is compatible with external systems or APIs.
|
126
|
+
|
127
|
+
In summary, the Model framework provides a powerful and flexible way to
|
128
|
+
define and enforce the structure of data models in a Ruby application, and
|
129
|
+
offers a variety of extension points for customizing the behavior of the
|
130
|
+
framework to meet the needs of specific use cases.
|
131
|
+
|
132
|
+
Hackety hacking, frens!
|
133
|
+
|
76
134
|
## Contributing
|
77
135
|
|
78
|
-
Bug reports and pull requests are welcome on GitHub at [link to GitHub repo](https://github.com/
|
136
|
+
Bug reports and pull requests are welcome on GitHub at [link to GitHub repo](https://github.com/akuhn/hamachi). This project encourages collaboration and appreciates contributions. Feel free to contribute to the project by reporting bugs or submitting pull requests.
|
79
137
|
|
80
138
|
## License
|
81
139
|
|
data/lib/hamachi/ext.rb
ADDED
data/lib/hamachi/model.rb
CHANGED
@@ -1,284 +1,3 @@
|
|
1
|
-
require '
|
1
|
+
require 'hamachi'
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
# This class can be used to create a flexible and type-safe representation of
|
6
|
-
# JSON data. It provides a convenient way to create and validate data models
|
7
|
-
# in Ruby, making it easier to build complex applications.
|
8
|
-
#
|
9
|
-
# The Model class extends the built-in Hash class and is designed to enforce
|
10
|
-
# type constraints on data objects that can be created from JSON snapshots. It
|
11
|
-
# defines custom syntax for declaring and validating fields, with support for
|
12
|
-
# common data types suchs enums, lists, and nullable types.
|
13
|
-
#
|
14
|
-
# Example usage
|
15
|
-
#
|
16
|
-
# class Person < Model
|
17
|
-
# field %{name}, type: String
|
18
|
-
# field %{gender}, type: (enum :male, :female)
|
19
|
-
# field %{age}, type: 1..100
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# anna = Person.new(
|
23
|
-
# name: 'Anna',
|
24
|
-
# gender: :female,
|
25
|
-
# age: 29,
|
26
|
-
# )
|
27
|
-
#
|
28
|
-
# Type checking in the Model framework is based on a combination of built-in
|
29
|
-
# Ruby functionality and custom matchers that are optimized for working with
|
30
|
-
# complex data structures.
|
31
|
-
#
|
32
|
-
# - The framework relies on the === operator, which is a built-in method in
|
33
|
-
# Ruby that checks whether a given value is a member of a class or matches
|
34
|
-
# a pattern, such as a regular-expression or a range of numbers
|
35
|
-
# - In addition the framework provides a set of custom matchers that are
|
36
|
-
# optimized for working with more complex data structures. These matchers
|
37
|
-
# include support for lists, nullable types, enumerations, and more.
|
38
|
-
#
|
39
|
-
# Another way to extend the type checking capabilities is by subclassing the
|
40
|
-
# Matcher class. This allows developers to create custom matchers that can
|
41
|
-
# validate complex data structures or enforce domain-specific rules on the
|
42
|
-
# values of fields in a model. This provides a powerful extension point that
|
43
|
-
# allows developers to meet the needs of their specific use cases, and can
|
44
|
-
# help ensure data quality and consistency in their applications.
|
45
|
-
#
|
46
|
-
# Customizing serialization is an important aspect of working with data models,
|
47
|
-
# and the Model framework provides a flexible way to achieve this through the
|
48
|
-
# to_json and from_snapshot methods. These methods allow developers to control
|
49
|
-
# how data is represented in JSON format, which can be important ensure that
|
50
|
-
# the serialized data is compatible with external systems or APIs.
|
51
|
-
#
|
52
|
-
# In summary, the Model framework provides a powerful and flexible way to
|
53
|
-
# define and enforce the structure of data models in a Ruby application, and
|
54
|
-
# offers a variety of extension points for customizing the behavior of the
|
55
|
-
# framework to meet the needs of specific use cases.
|
56
|
-
#
|
57
|
-
# Hackety hacking, frens!
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
class Hamachi::Model < Hash
|
63
|
-
|
64
|
-
NULL = Object.new
|
65
|
-
|
66
|
-
def initialize(snapshot, options = {})
|
67
|
-
update(snapshot) unless options.fetch(:ignore_undeclared_fields, false)
|
68
|
-
|
69
|
-
self.class.fields.each do |name, field|
|
70
|
-
value = snapshot.fetch(name, field.default_value)
|
71
|
-
self[name] = field.from_snapshot(value, options)
|
72
|
-
end
|
73
|
-
|
74
|
-
check_types if options.fetch(:check_types, true)
|
75
|
-
freeze if options.fetch(:freeze, false)
|
76
|
-
end
|
77
|
-
|
78
|
-
def self.from_snapshot(snapshot, options = {})
|
79
|
-
return snapshot unless Hash === snapshot
|
80
|
-
unless snapshot.keys.all? { |name| Symbol === name }
|
81
|
-
raise "expected names to be symbols, got other"
|
82
|
-
end
|
83
|
-
self.new snapshot, options
|
84
|
-
end
|
85
|
-
|
86
|
-
def self.from_json(str)
|
87
|
-
snapshot = JSON.parse str, symbolize_names: true
|
88
|
-
self.from_snapshot(snapshot, {})
|
89
|
-
end
|
90
|
-
|
91
|
-
def check_types
|
92
|
-
self.class.fields.each do |name, field|
|
93
|
-
if not field === self[name]
|
94
|
-
raise "expected #{name} to be #{field}, got #{self[name].inspect}"
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def prune_default_values
|
100
|
-
self.class.fields.each do |name, field|
|
101
|
-
case value = self[name]
|
102
|
-
when field.default_value
|
103
|
-
self.delete(name)
|
104
|
-
when Model
|
105
|
-
value.prune_default_values
|
106
|
-
when Array
|
107
|
-
value.each { |each| each.prune_default_values if Model === each }
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
return self
|
112
|
-
end
|
113
|
-
|
114
|
-
def self.fields
|
115
|
-
@fields ||= {}
|
116
|
-
end
|
117
|
-
|
118
|
-
def self.field(name, options)
|
119
|
-
raise "expected #{name} to be undefined, got method" if method_defined?(name.to_sym)
|
120
|
-
|
121
|
-
field = options.fetch(:type)
|
122
|
-
field = Matcher.new(field) unless Matcher === field
|
123
|
-
field.initialize_options(options).freeze
|
124
|
-
self.fields[name.to_sym] = field
|
125
|
-
|
126
|
-
class_eval %{
|
127
|
-
def #{name}
|
128
|
-
self[:#{name}]
|
129
|
-
end
|
130
|
-
}
|
131
|
-
|
132
|
-
class_eval %{
|
133
|
-
def #{name}=(value)
|
134
|
-
field = self.class.fields[:#{name}]
|
135
|
-
if not field === value
|
136
|
-
raise "expected #{name} to be \#{field}, got \#{value.inspect}"
|
137
|
-
end
|
138
|
-
self[:#{name}] = value
|
139
|
-
end
|
140
|
-
}
|
141
|
-
end
|
142
|
-
|
143
|
-
def self.define(&block) # for anonymous inline models
|
144
|
-
Class.new Hamachi::Model, &block
|
145
|
-
end
|
146
|
-
|
147
|
-
def self.to_s
|
148
|
-
name ? name : "model(#{fields.map { |name, field| "#{name}:#{field}"}.join(',')})"
|
149
|
-
end
|
150
|
-
|
151
|
-
|
152
|
-
# --- Helper methods for type declarations ------------------
|
153
|
-
|
154
|
-
def self.enum(*symbols)
|
155
|
-
EnumMatcher.new(symbols)
|
156
|
-
end
|
157
|
-
|
158
|
-
def self.list(type)
|
159
|
-
ListMatcher.new(type)
|
160
|
-
end
|
161
|
-
|
162
|
-
def self.nullable(type)
|
163
|
-
NullableMatcher.new(type)
|
164
|
-
end
|
165
|
-
|
166
|
-
def self.model(&block)
|
167
|
-
Hamachi::Model.define(&block)
|
168
|
-
end
|
169
|
-
|
170
|
-
def self.positive(type)
|
171
|
-
PositiveMatcher.new(type)
|
172
|
-
end
|
173
|
-
|
174
|
-
def self.positive_or_zero(type)
|
175
|
-
PositiveOrZeroMatcher.new(type)
|
176
|
-
end
|
177
|
-
|
178
|
-
|
179
|
-
# --- Matcher classes ---------------------------------------
|
180
|
-
|
181
|
-
class Matcher
|
182
|
-
def initialize(type)
|
183
|
-
@type = type
|
184
|
-
end
|
185
|
-
|
186
|
-
def initialize_options(options)
|
187
|
-
end
|
188
|
-
|
189
|
-
def ===(value)
|
190
|
-
@type === value
|
191
|
-
end
|
192
|
-
|
193
|
-
def default_value
|
194
|
-
nil
|
195
|
-
end
|
196
|
-
|
197
|
-
def to_s
|
198
|
-
@type.to_s
|
199
|
-
end
|
200
|
-
|
201
|
-
def from_snapshot(data, options)
|
202
|
-
if @type == Symbol
|
203
|
-
data.to_sym if data
|
204
|
-
elsif Class === @type && @type.respond_to?(:from_snapshot)
|
205
|
-
@type.from_snapshot(data, options)
|
206
|
-
else
|
207
|
-
data
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
class EnumMatcher < Matcher
|
213
|
-
def ===(value)
|
214
|
-
@type.any? { |each| each === value }
|
215
|
-
end
|
216
|
-
|
217
|
-
def to_s
|
218
|
-
"enum(#{@type.map(&:inspect).join(', ')})"
|
219
|
-
end
|
220
|
-
|
221
|
-
def from_snapshot(data, options)
|
222
|
-
String === data ? data.to_sym : data
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
class ListMatcher < Matcher
|
227
|
-
def initialize_options(options)
|
228
|
-
@option_empty = options.fetch(:empty, true)
|
229
|
-
end
|
230
|
-
|
231
|
-
def ===(value)
|
232
|
-
return false unless Array === value
|
233
|
-
return false if value.empty? unless @option_empty
|
234
|
-
value.all? { |each| @type === each }
|
235
|
-
end
|
236
|
-
|
237
|
-
def default_value
|
238
|
-
[]
|
239
|
-
end
|
240
|
-
|
241
|
-
def to_s
|
242
|
-
"list(#{@type}#{', empty: false' unless @option_empty})"
|
243
|
-
end
|
244
|
-
|
245
|
-
def from_snapshot(data, options)
|
246
|
-
data && data.map { |each| super(each, options) }
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
class NullableMatcher < Matcher
|
251
|
-
def ===(value)
|
252
|
-
@type === value || value.nil?
|
253
|
-
end
|
254
|
-
|
255
|
-
def to_s
|
256
|
-
"nullable(#{@type})"
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
class PositiveMatcher < Matcher
|
261
|
-
def ===(value)
|
262
|
-
@type === value && value.positive?
|
263
|
-
end
|
264
|
-
|
265
|
-
def to_s
|
266
|
-
"positive(#{@type})"
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
# FIXME: make this matcher class a module that can be included here
|
271
|
-
|
272
|
-
class PositiveOrZeroMatcher < Matcher
|
273
|
-
def ===(value)
|
274
|
-
@type === value && !value.negative?
|
275
|
-
end
|
276
|
-
|
277
|
-
def to_s
|
278
|
-
"positive_or_zero(#{@type})"
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
Boolean = enum(true, false)
|
283
|
-
Timestamp = Regexp.new(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/)
|
284
|
-
end
|
3
|
+
Model = Hamachi::Model
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Extension for arrays and other enumerables. It provides convenient and
|
4
|
+
# reusable functionality to manipulate and analyze enumerable objects in
|
5
|
+
# a concise and expressive manner.
|
6
|
+
|
7
|
+
|
8
|
+
module Hamachi
|
9
|
+
module EnumerableExt
|
10
|
+
def index_by
|
11
|
+
raise unless block_given?
|
12
|
+
|
13
|
+
index = Hash.new
|
14
|
+
self.each { |each| index[yield each] = each }
|
15
|
+
index
|
16
|
+
end
|
17
|
+
|
18
|
+
def freq
|
19
|
+
h = Hash.new(0)
|
20
|
+
if block_given?
|
21
|
+
each { |each| h[yield each] += 1 }
|
22
|
+
else
|
23
|
+
each { |each| h[each] += 1 }
|
24
|
+
end
|
25
|
+
h.sort_by(&:last).to_h
|
26
|
+
end
|
27
|
+
|
28
|
+
def where(patterns)
|
29
|
+
each.select { |each| patterns.all? { |symbol, pattern| pattern === each.send(symbol) }}
|
30
|
+
end
|
31
|
+
|
32
|
+
def wherent(patterns)
|
33
|
+
each.reject { |each| patterns.all? { |symbol, pattern| pattern === each.send(symbol) }}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Hamachi
|
5
|
+
|
6
|
+
class Field
|
7
|
+
def initialize(type)
|
8
|
+
@type = type
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_options(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def ===(value)
|
15
|
+
@type === value
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_value
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
@type.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def from_snapshot(data, options = nil)
|
27
|
+
if @type == Symbol
|
28
|
+
data.to_sym if data
|
29
|
+
elsif Class === @type && @type.respond_to?(:from_snapshot)
|
30
|
+
@type.from_snapshot(data, options)
|
31
|
+
else
|
32
|
+
data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class EnumField < Field
|
38
|
+
def initialize(*symbols)
|
39
|
+
super symbols
|
40
|
+
end
|
41
|
+
|
42
|
+
def ===(value)
|
43
|
+
@type.any? { |each| each === value }
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"enum(#{@type.map(&:inspect).join(?,)})"
|
48
|
+
end
|
49
|
+
|
50
|
+
def from_snapshot(data, options)
|
51
|
+
String === data ? data.to_sym : data
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class ListField < Field
|
56
|
+
def initialize_options(options)
|
57
|
+
@option_empty = options.fetch(:empty, true)
|
58
|
+
end
|
59
|
+
|
60
|
+
def ===(value)
|
61
|
+
return false unless Array === value
|
62
|
+
return false if value.empty? unless @option_empty
|
63
|
+
value.all? { |each| @type === each }
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_value
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"list(#{@type}#{', empty: false' unless @option_empty})"
|
72
|
+
end
|
73
|
+
|
74
|
+
def from_snapshot(data, options)
|
75
|
+
data && data.map { |each| super(each, options) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class NullableField < Field
|
80
|
+
def ===(value)
|
81
|
+
@type === value || value.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"nullable(#{@type})"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
|
6
|
+
module Hamachi
|
7
|
+
class Model < Hash
|
8
|
+
|
9
|
+
# ------- schema declaration ---------------------------------------
|
10
|
+
|
11
|
+
def self.fields
|
12
|
+
@fields ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.field(name, options)
|
16
|
+
raise ArgumentError, "method ##{name} already defined" if method_defined?(name)
|
17
|
+
raise ArgumentError, "method ##{name}= already defined" if method_defined?("#{name}=")
|
18
|
+
|
19
|
+
field = options.fetch(:type)
|
20
|
+
field = Field.new(field) unless Field === field
|
21
|
+
field.initialize_options(options).freeze
|
22
|
+
self.fields[name.to_sym] = field
|
23
|
+
|
24
|
+
class_eval %{
|
25
|
+
def #{name}
|
26
|
+
self[:#{name}]
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
class_eval %{
|
31
|
+
def #{name}=(value)
|
32
|
+
field = self.class.fields[:#{name}]
|
33
|
+
if not field === value
|
34
|
+
raise "expected #{name} to be \#{field}, got \#{value.inspect}"
|
35
|
+
end
|
36
|
+
self[:#{name}] = value
|
37
|
+
end
|
38
|
+
}
|
39
|
+
|
40
|
+
return self
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.schema(&block) # for anonymous inline models
|
44
|
+
Class.new Hamachi::Model, &block
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.to_s
|
48
|
+
name ? name : "schema(#{fields.map { |name, field| "#{name}:#{field}"}.join(',')})"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.register_type(name, field_class)
|
52
|
+
singleton_class.send(:define_method, name) do |*args|
|
53
|
+
field_class.new(*args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
register_type :list, ListField
|
58
|
+
register_type :nullable, NullableField
|
59
|
+
register_type :enum, EnumField
|
60
|
+
|
61
|
+
Boolean = enum(true, false)
|
62
|
+
|
63
|
+
|
64
|
+
# ------- initialization -------------------------------------------
|
65
|
+
|
66
|
+
def initialize(snapshot, options = {})
|
67
|
+
update(snapshot) if options.fetch(:include_unknown_fields, true)
|
68
|
+
|
69
|
+
self.class.fields.each do |name, field|
|
70
|
+
value = snapshot.fetch(name, field.default_value)
|
71
|
+
self[name] = field.from_snapshot(value, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
validate_fields! if options.fetch(:validate_fields, true)
|
75
|
+
freeze if options.fetch(:freeze, false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.from_snapshot(snapshot, options = {})
|
79
|
+
return snapshot unless Hash === snapshot
|
80
|
+
self.new snapshot, options
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.parse(string, options = {})
|
84
|
+
snapshot = JSON.parse(string, symbolize_names: true)
|
85
|
+
if Array === snapshot
|
86
|
+
snapshot.map { |each| from_snapshot each, options }
|
87
|
+
else
|
88
|
+
from_snapshot snapshot, options
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# ------- validation -----------------------------------------------
|
94
|
+
|
95
|
+
def valid?
|
96
|
+
gen_error_messages { return false }
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_fields!
|
101
|
+
gen_error_messages { |error| raise error }
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def gen_error_messages
|
107
|
+
self.class.fields.each do |name, field|
|
108
|
+
if not field === self[name]
|
109
|
+
yield "expected #{name} to be #{field}, got #{self[name].inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/hamachi/version.rb
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hamachi
|
4
|
-
VERSION = "0.1
|
4
|
+
VERSION = "0.3.1"
|
5
5
|
end
|
6
|
+
|
7
|
+
__END__
|
8
|
+
|
9
|
+
# Major version bump when breaking changes or new features
|
10
|
+
# Minor version bump when backward-compatible changes or enhancements
|
11
|
+
# Patch version bump when backward-compatible bug fixes, security updates etc
|
12
|
+
|
13
|
+
|
14
|
+
0.3.1
|
15
|
+
|
16
|
+
- Rename check_types to valid? and validate_fields!
|
17
|
+
- Reorganize tests and write more tests
|
18
|
+
|
19
|
+
0.3.0
|
20
|
+
|
21
|
+
- Rename Matcher class to Field class
|
22
|
+
- Rename Model.register_matcher to Model.register_type
|
23
|
+
|
24
|
+
0.2.0
|
25
|
+
|
26
|
+
- Require 'hamachi/model' to import model as top-level constant
|
27
|
+
- Require 'hamachi/ext' to extend arrays and other enumerables
|
28
|
+
- New function Model.register_matcher
|
29
|
+
|
30
|
+
0.1.0
|
31
|
+
|
32
|
+
- Initial import from internal project.
|
data/lib/hamachi.rb
CHANGED
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hamachi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Kuhn
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-06-
|
11
|
+
date: 2023-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description:
|
14
14
|
email:
|
15
15
|
- akuhn@iam.unibe.ch
|
16
16
|
executables: []
|
@@ -19,7 +19,11 @@ extra_rdoc_files: []
|
|
19
19
|
files:
|
20
20
|
- README.md
|
21
21
|
- lib/hamachi.rb
|
22
|
+
- lib/hamachi/ext.rb
|
22
23
|
- lib/hamachi/model.rb
|
24
|
+
- lib/hamachi/source/enumerable_ext.rb
|
25
|
+
- lib/hamachi/source/field.rb
|
26
|
+
- lib/hamachi/source/model.rb
|
23
27
|
- lib/hamachi/version.rb
|
24
28
|
homepage: https://github.com/akuhn/hamachi
|
25
29
|
licenses: []
|
@@ -27,7 +31,7 @@ metadata:
|
|
27
31
|
homepage_uri: https://github.com/akuhn/hamachi
|
28
32
|
source_code_uri: https://github.com/akuhn/hamachi
|
29
33
|
changelog_uri: https://github.com/akuhn/hamachi/blob/master/lib/hamachi/version.rb
|
30
|
-
post_install_message:
|
34
|
+
post_install_message:
|
31
35
|
rdoc_options: []
|
32
36
|
require_paths:
|
33
37
|
- lib
|
@@ -42,8 +46,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
46
|
- !ruby/object:Gem::Version
|
43
47
|
version: '0'
|
44
48
|
requirements: []
|
45
|
-
rubygems_version: 3.
|
46
|
-
signing_key:
|
49
|
+
rubygems_version: 3.1.6
|
50
|
+
signing_key:
|
47
51
|
specification_version: 4
|
48
52
|
summary: Flexible and type-safe representation of JSON data.
|
49
53
|
test_files: []
|