hamachi 0.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 +7 -0
- data/README.md +82 -0
- data/lib/hamachi/model.rb +284 -0
- data/lib/hamachi/version.rb +5 -0
- data/lib/hamachi.rb +9 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: be0e3b8a58a8efb76f493e85f2e034865451805dd57a28f461acee57b15618b9
|
4
|
+
data.tar.gz: d4348290d01e448676f5cff5be31d0246e6651be4d869e320df5c260d3e45421
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8edb8139725830afef0cd9cdda66a4a93a3b7debb3f3753fc57e43c16c3f7d86f38fe8d9768eb3854795e512fcd988c2c761bfab9c7510e6f3f1dbdcdd263cb2
|
7
|
+
data.tar.gz: 04d6c9d8ae09700e9981d1901bf10b9d2bf30c73fc33534d3f69bb9b776d33eabbfca8fe1099e126743eda7d764fa9bbeda0c4179cb148bf706298c6fd185ff7
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Hamachi
|
2
|
+
|
3
|
+
Hamachi is a Ruby library designed to simplify the creation and manipulation of domain-specific data models, supporting type checking, data validation, and JSON deserialization. This library takes advantage of Ruby's dynamic nature, providing a fluent and intuitive interface to define domain models.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- Dynamic model creation with a flexible field declaration syntax.
|
8
|
+
- Type checking and enforcement to ensure model validity.
|
9
|
+
- Simple JSON to Model deserialization.
|
10
|
+
- Easy access to model data using accessor methods.
|
11
|
+
- Nullability, enumerations, lists, and other constraints.
|
12
|
+
- Custom model matching classes for extending the library's capabilities.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'hamachi'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle install
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install hamachi
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Here is a basic usage example:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'hamachi'
|
36
|
+
|
37
|
+
class User < Hamachi::Model
|
38
|
+
field :name, type: String
|
39
|
+
field :age, type: 1..100
|
40
|
+
end
|
41
|
+
|
42
|
+
user = User.from_json('{"name": "Alice", "age": 30}')
|
43
|
+
user = User.new(name: "Alice", age: 30)
|
44
|
+
|
45
|
+
user.name = 'Bob'
|
46
|
+
user.age = 8000 # => raises TypeError
|
47
|
+
```
|
48
|
+
|
49
|
+
You can define the following types of fields:
|
50
|
+
|
51
|
+
- Basic types (e.g. `String`, `Integer`, `Float`, `Symbol`, `Boolean`)
|
52
|
+
- Enumerations (e.g. `enum(:admin, :user, :guest)`)
|
53
|
+
- Lists of certain type (e.g. `list(String)`, `list(User)`)
|
54
|
+
- Nullable fields (e.g. `nullable(String)`, `nullable(User)`)
|
55
|
+
- Positive value fields (e.g. `positive(Integer)`, `positive(Float)`)
|
56
|
+
- Regular expressions (e.g. `/\A\d\d\d\d-\d\d-\d\d\z/` for matching dates)
|
57
|
+
- Ranges (e.g. `1..100` for matching integers between 1 and 100)
|
58
|
+
|
59
|
+
More complex nested models can be created:
|
60
|
+
|
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
|
+
class Post < Hamachi::Model
|
69
|
+
field :title, type: String
|
70
|
+
field :content, type: String
|
71
|
+
field :created_at, type: Timestamp
|
72
|
+
field :tags, type: list(String)
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
Bug reports and pull requests are welcome on GitHub at [link to GitHub repo](https://github.com/yourusername/your-repo). This project encourages collaboration and appreciates contributions. Feel free to contribute to the project by reporting bugs or submitting pull requests.
|
79
|
+
|
80
|
+
## License
|
81
|
+
|
82
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# A model has type-checked fields.
|
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
|
data/lib/hamachi.rb
ADDED
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hamachi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adrian Kuhn
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- akuhn@iam.unibe.ch
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/hamachi.rb
|
22
|
+
- lib/hamachi/model.rb
|
23
|
+
- lib/hamachi/version.rb
|
24
|
+
homepage: https://github.com/akuhn/hamachi
|
25
|
+
licenses: []
|
26
|
+
metadata:
|
27
|
+
homepage_uri: https://github.com/akuhn/hamachi
|
28
|
+
source_code_uri: https://github.com/akuhn/hamachi
|
29
|
+
changelog_uri: https://github.com/akuhn/hamachi/blob/master/lib/hamachi/version.rb
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 1.9.3
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.3.7
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Flexible and type-safe representation of JSON data.
|
49
|
+
test_files: []
|