hamachi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|