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 +7 -0
- data/README.md +136 -0
- data/lib/hash_with_field_validation/field.rb +88 -0
- data/lib/hash_with_field_validation/model.rb +3 -0
- data/lib/hash_with_field_validation/version.rb +39 -0
- data/lib/hash_with_field_validation.rb +117 -0
- metadata +50 -0
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,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: []
|