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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be0e3b8a58a8efb76f493e85f2e034865451805dd57a28f461acee57b15618b9
4
- data.tar.gz: d4348290d01e448676f5cff5be31d0246e6651be4d869e320df5c260d3e45421
3
+ metadata.gz: 7664640e09c9177c316943760e26eb565dd556a22c6ebfb01708e95e57724619
4
+ data.tar.gz: 17596dac61e26f9d173a129571f999b81ff395faea800a3c9b33a668536820c5
5
5
  SHA512:
6
- metadata.gz: 8edb8139725830afef0cd9cdda66a4a93a3b7debb3f3753fc57e43c16c3f7d86f38fe8d9768eb3854795e512fcd988c2c761bfab9c7510e6f3f1dbdcdd263cb2
7
- data.tar.gz: 04d6c9d8ae09700e9981d1901bf10b9d2bf30c73fc33534d3f69bb9b776d33eabbfca8fe1099e126743eda7d764fa9bbeda0c4179cb148bf706298c6fd185ff7
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 = 8000 # => raises TypeError
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/yourusername/your-repo). This project encourages collaboration and appreciates contributions. Feel free to contribute to the project by reporting bugs or submitting pull requests.
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
 
@@ -0,0 +1,3 @@
1
+ require 'hamachi/source/enumerable_ext'
2
+
3
+ Enumerable.include Hamachi::EnumerableExt
data/lib/hamachi/model.rb CHANGED
@@ -1,284 +1,3 @@
1
- require 'json'
1
+ require 'hamachi'
2
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
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
@@ -1,5 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hamachi
4
- VERSION = "0.1.0"
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
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hamachi/version"
4
- require "hamachi/model"
5
-
6
-
7
- module Hamachi
8
- # pass
9
- end
3
+ require 'hamachi/version'
4
+ require 'hamachi/source/field'
5
+ require 'hamachi/source/model'
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.0
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-02 00:00:00.000000000 Z
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.3.7
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: []