hamachi 0.1.0 → 0.3.1

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