hash_with_field_validation 1.0.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: b82063b5a2c11b94105d32b760ba9b3033dea4a833c4f67f1e88a2f64a4308ab
4
+ data.tar.gz: bcde13510ea967e22f64c703b311a4719e584d63a1e5d622e4e7a9235f11bbdf
5
+ SHA512:
6
+ metadata.gz: 55aab9213de1a42184f497001573aa264b82577e76d4b164277f41d89720576094a694cf2f57bfd8ab1ae6b30f154b7f67bd813d21756c1c99e835f6effcc5d5
7
+ data.tar.gz: e6b772e7e61c6c4fde88d18a74a48fc0256c28c2af1c45737b7300db9065330352eb11dc078fcd4cf41d2f1f1b46d80e29fd2bf62f5ffc3465d5df86307a4230
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # HashWithFieldValidation
2
+
3
+ HashWithFieldValidation 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 'hash_with_field_validation'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle install
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install hash_with_field_validation
29
+
30
+ ## Usage
31
+
32
+ Here is a basic usage example:
33
+
34
+ ```ruby
35
+ require 'hash_with_field_validation'
36
+
37
+ class User < HashWithFieldValidation
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 = 120 # => raises RuntimeError: expected age to be 1..100, got 120
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 Post < HashWithFieldValidation
63
+ field :title, type: String
64
+ field :content, type: String
65
+ field :created_at, type: Timestamp
66
+ field :tags, type: list(String)
67
+ end
68
+
69
+ class User < HashWithFieldValidation
70
+ field :name, type: String
71
+ field :friends, type: list(User)
72
+ field :posts, type: list(Post)
73
+ end
74
+ ```
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
+
134
+ ## Contributing
135
+
136
+ Bug reports and pull requests are welcome on GitHub at [link to GitHub repo](https://github.com/akuhn/hash_with_field_validation). This project encourages collaboration and appreciates contributions. Feel free to contribute to the project by reporting bugs or submitting pull requests.
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ class HashWithFieldValidation < Hash
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,3 @@
1
+ require 'hash_with_field_validation'
2
+
3
+ Model = HashWithFieldValidation
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HashWithFieldValidation < Hash
4
+ VERSION = "1.0.0"
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
+ 1.0.0
14
+
15
+ - Renamed to hash_with_field_validation
16
+ - Make the former model class the top-level element
17
+ - Removed enumerable extensions, see another gem
18
+ - New function Model#error_messages
19
+ - Reorganize tests and write more tests
20
+
21
+ 0.3.1
22
+
23
+ - Rename check_types to valid? and validate_fields!
24
+ - Reorganize tests and write more tests
25
+
26
+ 0.3.0
27
+
28
+ - Rename Matcher class to Field class
29
+ - Rename Model.register_matcher to Model.register_type
30
+
31
+ 0.2.0
32
+
33
+ - Require 'hamachi/model' to import model as top-level constant
34
+ - Require 'hamachi/ext' to extend arrays and other enumerables
35
+ - New function Model.register_matcher
36
+
37
+ 0.1.0
38
+
39
+ - Initial import from internal project.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hash_with_field_validation/version'
4
+ require 'hash_with_field_validation/field'
5
+
6
+
7
+ class HashWithFieldValidation < 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 HashWithFieldValidation, &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
+ def error_messages
105
+ enum_for(:gen_error_messages).entries
106
+ end
107
+
108
+ private
109
+
110
+ def gen_error_messages
111
+ self.class.fields.each do |name, field|
112
+ if not field === self[name]
113
+ yield "expected #{name} to be #{field}, got #{self[name].inspect}"
114
+ end
115
+ end
116
+ end
117
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_with_field_validation
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - A Kuhn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-14 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/hash_with_field_validation.rb
22
+ - lib/hash_with_field_validation/field.rb
23
+ - lib/hash_with_field_validation/model.rb
24
+ - lib/hash_with_field_validation/version.rb
25
+ homepage: https://github.com/akuhn/hash_with_field_validation
26
+ licenses: []
27
+ metadata:
28
+ homepage_uri: https://github.com/akuhn/hash_with_field_validation
29
+ source_code_uri: https://github.com/akuhn/hash_with_field_validation
30
+ changelog_uri: https://github.com/akuhn/hash_with_field_validation/blob/master/lib/hash_with_field_validation/version.rb
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.9.3
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.0.3.1
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Flexible and type-safe representation of JSON data.
50
+ test_files: []