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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hamachi
4
+ VERSION = "0.1.0"
5
+ end
data/lib/hamachi.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hamachi/version"
4
+ require "hamachi/model"
5
+
6
+
7
+ module Hamachi
8
+ # pass
9
+ end
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: []