hamachi 0.1.0

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