hamachi 0.1.0 → 0.3.1
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 +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: []
|