scheming 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +12 -0
- data/lib/scheming/attribute/list.rb +51 -0
- data/lib/scheming/attribute/list_builder.rb +26 -0
- data/lib/scheming/attribute.rb +30 -0
- data/lib/scheming/dsl/data_builder.rb +32 -0
- data/lib/scheming/dsl/object_type_def.rb +36 -0
- data/lib/scheming/dsl/type_resolver.rb +90 -0
- data/lib/scheming/dsl/type_specs.rb +29 -0
- data/lib/scheming/dsl.rb +15 -0
- data/lib/scheming/schema/json.rb +104 -0
- data/lib/scheming/schema.rb +22 -0
- data/lib/scheming/type.rb +100 -0
- data/lib/scheming/version.rb +5 -0
- data/lib/scheming.rb +21 -0
- data/sig/scheming.rbs +4 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a71514dd3a8b1f9640a40adf0bbfd928cab9abe7b6981fad8ac0e25a1c7add01
|
4
|
+
data.tar.gz: edad4ab18d9c22efebbdc39b9a7c74d451e9a41b9b85892a743bb0dc726301b2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: daaff46c5331a1dd747fadfd50701f497225d05104d443dce75c0b8d244445943cea917e5d2ad39a2daf2fbe271ea08edd0930ac351a12dd1ff552c05b6fb590
|
7
|
+
data.tar.gz: 28f7756667869ff46d076f74dc89849d65a5fc3609c2c472d85f02cfe16143c74936ac7d16bb2d577650b53df159d9402d34926999f6e8aa3808d721dec2afc9
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
AllCops:
|
2
|
+
SuggestExtensions: false
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
Style/StringLiterals:
|
6
|
+
Enabled: true
|
7
|
+
EnforcedStyle: single_quotes
|
8
|
+
|
9
|
+
Metrics/BlockLength:
|
10
|
+
AllowedMethods: ['define', 'describe', 'it', 'context', 'let']
|
11
|
+
|
12
|
+
Style/StringLiteralsInInterpolation:
|
13
|
+
Enabled: true
|
14
|
+
EnforcedStyle: double_quotes
|
15
|
+
|
16
|
+
Style/ClassAndModuleChildren:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Layout/LineLength:
|
20
|
+
Max: 90
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in dee_tea_ohh.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'factory_bot', '~> 6.4'
|
9
|
+
gem 'pry', '~> 0.14.2'
|
10
|
+
gem 'rake', '~> 13.0'
|
11
|
+
gem 'rspec', '~> 3.0'
|
12
|
+
gem 'rubocop', '~> 1.21'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
scheming (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (7.1.3.2)
|
10
|
+
base64
|
11
|
+
bigdecimal
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
connection_pool (>= 2.2.5)
|
14
|
+
drb
|
15
|
+
i18n (>= 1.6, < 2)
|
16
|
+
minitest (>= 5.1)
|
17
|
+
mutex_m
|
18
|
+
tzinfo (~> 2.0)
|
19
|
+
ast (2.4.2)
|
20
|
+
base64 (0.2.0)
|
21
|
+
bigdecimal (3.1.7)
|
22
|
+
coderay (1.1.3)
|
23
|
+
concurrent-ruby (1.2.3)
|
24
|
+
connection_pool (2.4.1)
|
25
|
+
diff-lcs (1.5.1)
|
26
|
+
drb (2.2.1)
|
27
|
+
factory_bot (6.4.6)
|
28
|
+
activesupport (>= 5.0.0)
|
29
|
+
i18n (1.14.4)
|
30
|
+
concurrent-ruby (~> 1.0)
|
31
|
+
json (2.7.2)
|
32
|
+
language_server-protocol (3.17.0.3)
|
33
|
+
method_source (1.1.0)
|
34
|
+
minitest (5.22.3)
|
35
|
+
mutex_m (0.2.0)
|
36
|
+
parallel (1.24.0)
|
37
|
+
parser (3.3.1.0)
|
38
|
+
ast (~> 2.4.1)
|
39
|
+
racc
|
40
|
+
pry (0.14.2)
|
41
|
+
coderay (~> 1.1)
|
42
|
+
method_source (~> 1.0)
|
43
|
+
racc (1.7.3)
|
44
|
+
rainbow (3.1.1)
|
45
|
+
rake (13.2.1)
|
46
|
+
regexp_parser (2.9.0)
|
47
|
+
rexml (3.2.6)
|
48
|
+
rspec (3.13.0)
|
49
|
+
rspec-core (~> 3.13.0)
|
50
|
+
rspec-expectations (~> 3.13.0)
|
51
|
+
rspec-mocks (~> 3.13.0)
|
52
|
+
rspec-core (3.13.0)
|
53
|
+
rspec-support (~> 3.13.0)
|
54
|
+
rspec-expectations (3.13.0)
|
55
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
+
rspec-support (~> 3.13.0)
|
57
|
+
rspec-mocks (3.13.0)
|
58
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
59
|
+
rspec-support (~> 3.13.0)
|
60
|
+
rspec-support (3.13.1)
|
61
|
+
rubocop (1.63.3)
|
62
|
+
json (~> 2.3)
|
63
|
+
language_server-protocol (>= 3.17.0)
|
64
|
+
parallel (~> 1.10)
|
65
|
+
parser (>= 3.3.0.2)
|
66
|
+
rainbow (>= 2.2.2, < 4.0)
|
67
|
+
regexp_parser (>= 1.8, < 3.0)
|
68
|
+
rexml (>= 3.2.5, < 4.0)
|
69
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
70
|
+
ruby-progressbar (~> 1.7)
|
71
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
72
|
+
rubocop-ast (1.31.2)
|
73
|
+
parser (>= 3.3.0.4)
|
74
|
+
ruby-progressbar (1.13.0)
|
75
|
+
tzinfo (2.0.6)
|
76
|
+
concurrent-ruby (~> 1.0)
|
77
|
+
unicode-display_width (2.5.0)
|
78
|
+
|
79
|
+
PLATFORMS
|
80
|
+
x86_64-linux
|
81
|
+
|
82
|
+
DEPENDENCIES
|
83
|
+
factory_bot (~> 6.4)
|
84
|
+
pry (~> 0.14.2)
|
85
|
+
rake (~> 13.0)
|
86
|
+
rspec (~> 3.0)
|
87
|
+
rubocop (~> 1.21)
|
88
|
+
scheming!
|
89
|
+
|
90
|
+
BUNDLED WITH
|
91
|
+
2.4.5
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Ben Falk
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Scheming
|
2
|
+
|
3
|
+
Ergonomically define and work with data in Ruby
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'scheming'
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
### Simple Schema
|
14
|
+
|
15
|
+
Definition:
|
16
|
+
```ruby
|
17
|
+
LineItem = Scheming.object do
|
18
|
+
attribute :id, Integer
|
19
|
+
attribute :name, String
|
20
|
+
attribute :taxable, :bool
|
21
|
+
attribute :desc, Nullable(String)
|
22
|
+
attribute :price, Float
|
23
|
+
attribute :type, Enum('entertainment', 'staple')
|
24
|
+
end
|
25
|
+
|
26
|
+
Receipt = Scheming.object do
|
27
|
+
attribute :line_items, Array(LineItem)
|
28
|
+
attribute :total, Float
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
Example:
|
33
|
+
```ruby
|
34
|
+
Scheming::Schema.json(Receipt)
|
35
|
+
# =>
|
36
|
+
{
|
37
|
+
type: 'object',
|
38
|
+
additionalProperties: false,
|
39
|
+
required: %i[line_items total],
|
40
|
+
properties: {
|
41
|
+
line_items: {
|
42
|
+
type: 'array',
|
43
|
+
items: {
|
44
|
+
type: 'object',
|
45
|
+
additionalProperties: false,
|
46
|
+
required: %i[id name taxable desc price item_type],
|
47
|
+
properties: {
|
48
|
+
id: { type: 'integer' },
|
49
|
+
name: { type: 'string' },
|
50
|
+
taxable: { type: 'boolean' },
|
51
|
+
desc: {
|
52
|
+
oneOf: [
|
53
|
+
{ type: 'string' },
|
54
|
+
{ type: 'null' }
|
55
|
+
]
|
56
|
+
},
|
57
|
+
price: { type: 'numeric' },
|
58
|
+
item_type: {
|
59
|
+
type: 'string',
|
60
|
+
enum: %w[entertainment staple].to_set
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
},
|
65
|
+
total: { type: 'numeric' }
|
66
|
+
}
|
67
|
+
}
|
68
|
+
```
|
69
|
+
|
70
|
+
## Development
|
71
|
+
|
72
|
+
### Getting Started
|
73
|
+
|
74
|
+
To get going the following commands are helpful:
|
75
|
+
|
76
|
+
```bash
|
77
|
+
git clone https://github.com/benfalk/scheming.git
|
78
|
+
./scheming/setup
|
79
|
+
cd scheming
|
80
|
+
bundle exec rake
|
81
|
+
```
|
82
|
+
|
83
|
+
### Installing Locally
|
84
|
+
|
85
|
+
To install this gem onto your local machine:
|
86
|
+
|
87
|
+
```bash
|
88
|
+
bundle exec rake install
|
89
|
+
```
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
Bug reports and pull requests are welcome
|
94
|
+
on GitHub at https://github.com/benfalk/scheming.
|
95
|
+
|
96
|
+
## License
|
97
|
+
|
98
|
+
The gem is available as open source under the terms of
|
99
|
+
the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Attribute Collection
|
4
|
+
#
|
5
|
+
class Scheming::Attribute::List
|
6
|
+
class MissingAttribute < Scheming::Error; end
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @return [Scheming::Attribute::List]
|
10
|
+
def self.empty = @empty ||= new([])
|
11
|
+
|
12
|
+
# @param attributes [Array<Scheming::Attribute>]
|
13
|
+
def initialize(attributes = [])
|
14
|
+
@attributes = attributes.uniq(&:field_name).freeze
|
15
|
+
|
16
|
+
# @type [Hash<Symbol, Scheming::Attribute>]
|
17
|
+
@lookup =
|
18
|
+
@attributes
|
19
|
+
.each_with_object({}) do |attr, lookup|
|
20
|
+
lookup[attr.field_name] = attr
|
21
|
+
end
|
22
|
+
.freeze
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param key [Symbol]
|
27
|
+
# @return [Scheming::Attribute]
|
28
|
+
def attr(key)
|
29
|
+
@lookup.fetch(key) do |attr_key|
|
30
|
+
raise MissingAttribute, <<~MSG.strip!
|
31
|
+
Missing Attribute [#{attr_key}]
|
32
|
+
Available Fields:
|
33
|
+
#{@attributes.map(&:field_name).join("\n ")}
|
34
|
+
MSG
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias [] attr
|
38
|
+
|
39
|
+
def required = each.select(&:is_required)
|
40
|
+
|
41
|
+
# @return [Hash<Symbol, Scheming::Attribute>]
|
42
|
+
def to_h = @lookup
|
43
|
+
|
44
|
+
# Interface for Enumerable
|
45
|
+
# @yieldparam [Scheming::Attribute]
|
46
|
+
def each(&)
|
47
|
+
return enum_for(:each) unless block_given?
|
48
|
+
|
49
|
+
@attributes.each(&)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Attribute Collection Builder
|
4
|
+
#
|
5
|
+
class Scheming::Attribute::ListBuilder
|
6
|
+
# @param attributes [Array<Scheming::Attribute>]
|
7
|
+
def initialize(attributes = [])
|
8
|
+
@attributes = attributes
|
9
|
+
freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param field_name [Symbol]
|
13
|
+
# @param type [Scheming::Type::Base]
|
14
|
+
# @return [Scheming::Attribute::ListBuilder]
|
15
|
+
def attribute(field_name, type:, is_required: true)
|
16
|
+
attr = Scheming::Attribute.new(
|
17
|
+
field_name:,
|
18
|
+
type:,
|
19
|
+
is_required:
|
20
|
+
)
|
21
|
+
self.class.new(@attributes + [attr])
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Scheming::Attribute::List]
|
25
|
+
def build = Scheming::Attribute::List.new(@attributes)
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Attribute
|
4
|
+
#
|
5
|
+
class Scheming::Attribute
|
6
|
+
# @return [Symbol]
|
7
|
+
attr_reader :field_name
|
8
|
+
|
9
|
+
# @return [Scheming::Type::Base]
|
10
|
+
attr_reader :type
|
11
|
+
|
12
|
+
# @return [Boolean]
|
13
|
+
attr_reader :is_required
|
14
|
+
|
15
|
+
# @param field_name [Symbol]
|
16
|
+
# @param type [Scheming::Type::Base]
|
17
|
+
def initialize(
|
18
|
+
field_name:,
|
19
|
+
type:,
|
20
|
+
is_required: true
|
21
|
+
)
|
22
|
+
@field_name = field_name
|
23
|
+
@type = type
|
24
|
+
@is_required = is_required
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
require_relative 'attribute/list'
|
29
|
+
require_relative 'attribute/list_builder'
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Data Builder
|
4
|
+
class Scheming::DSL::DataBuilder
|
5
|
+
include Scheming::DSL::TypeSpecs
|
6
|
+
|
7
|
+
def initialize(builder = Scheming::Attribute::ListBuilder.new)
|
8
|
+
@builder = builder
|
9
|
+
@resolver = Scheming::DSL::TypeResolver
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param field_name [Symbol]
|
13
|
+
# @param type_spec [Object]
|
14
|
+
# @param null [Boolean]
|
15
|
+
# @return [void]
|
16
|
+
def attribute(field_name, type_spec)
|
17
|
+
type = @resolver.resolve(type_spec)
|
18
|
+
@builder = @builder.attribute(field_name, type:)
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Class]
|
23
|
+
def build
|
24
|
+
list = @builder.build
|
25
|
+
dto_type = Scheming::Type::Object.new(list)
|
26
|
+
|
27
|
+
data = ::Data.define(*list.map(&:field_name))
|
28
|
+
data.instance_variable_set(:@dto_type, dto_type)
|
29
|
+
data.include(Scheming::DSL::ObjectTypeDef)
|
30
|
+
data
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Object Definition Tag
|
4
|
+
#
|
5
|
+
# This serves as a tag for produced
|
6
|
+
# transformation object definitions.
|
7
|
+
#
|
8
|
+
module Scheming::DSL::ObjectTypeDef
|
9
|
+
# @private
|
10
|
+
module ClassMethods
|
11
|
+
# @return [Scheming::Type::Object]
|
12
|
+
def dto_type = @dto_type
|
13
|
+
|
14
|
+
def inherited(klass)
|
15
|
+
super
|
16
|
+
klass.extend(ClassMethods)
|
17
|
+
klass.instance_variable_set(
|
18
|
+
:@dto_type,
|
19
|
+
dto_type
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Class<Data>]
|
24
|
+
def extend_with(&)
|
25
|
+
list = Scheming::Attribute::ListBuilder.new(
|
26
|
+
dto_type.attributes.to_a
|
27
|
+
)
|
28
|
+
builder = Scheming::DSL::DataBuilder.new(list)
|
29
|
+
builder.instance_exec(&)
|
30
|
+
builder.build
|
31
|
+
end
|
32
|
+
end
|
33
|
+
private_constant :ClassMethods
|
34
|
+
|
35
|
+
def self.included(klass) = klass.extend(ClassMethods)
|
36
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Type Resolver
|
4
|
+
module Scheming::DSL::TypeResolver
|
5
|
+
module Constants
|
6
|
+
STRING = Scheming::Type::String.new.freeze
|
7
|
+
FLOAT = Scheming::Type::Float.new.freeze
|
8
|
+
INTEGER = Scheming::Type::Integer.new.freeze
|
9
|
+
BOOLEAN = Scheming::Type::Boolean.new.freeze
|
10
|
+
end
|
11
|
+
private_constant :Constants
|
12
|
+
|
13
|
+
refine Kernel do
|
14
|
+
import_methods Scheming::DSL::TypeSpecs
|
15
|
+
end
|
16
|
+
|
17
|
+
refine Symbol do
|
18
|
+
def dto_type
|
19
|
+
case self
|
20
|
+
when :int, :integer then Scheming::Type::Integer.new
|
21
|
+
when :str, :string then Scheming::Type::String.new
|
22
|
+
when :float then Scheming::Type::Float.new
|
23
|
+
when :bool then Scheming::Type::Boolean.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
refine Scheming::Type::Base do
|
29
|
+
def dto_type = self
|
30
|
+
end
|
31
|
+
|
32
|
+
refine Array do
|
33
|
+
def dto_type
|
34
|
+
# TODO: Error Handling
|
35
|
+
Scheming::Type::Array.new(first.dto_type)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
refine Scheming::DSL::ObjectTypeDef do
|
40
|
+
def dto_type = self.class.dto_type
|
41
|
+
end
|
42
|
+
|
43
|
+
refine ::String.singleton_class do
|
44
|
+
def dto_type = Constants::STRING
|
45
|
+
end
|
46
|
+
|
47
|
+
refine ::String do
|
48
|
+
def dto_type = Constants::STRING
|
49
|
+
end
|
50
|
+
|
51
|
+
refine ::Float.singleton_class do
|
52
|
+
def dto_type = Constants::FLOAT
|
53
|
+
end
|
54
|
+
|
55
|
+
refine ::Float do
|
56
|
+
def dto_type = Constants::FLOAT
|
57
|
+
end
|
58
|
+
|
59
|
+
refine ::Integer.singleton_class do
|
60
|
+
def dto_type = Constants::INTEGER
|
61
|
+
end
|
62
|
+
|
63
|
+
refine ::Integer do
|
64
|
+
def dto_type = Constants::INTEGER
|
65
|
+
end
|
66
|
+
|
67
|
+
refine ::TrueClass do
|
68
|
+
def dto_type = Constants::BOOLEAN
|
69
|
+
end
|
70
|
+
|
71
|
+
refine ::FalseClass do
|
72
|
+
def dto_type = Constants::BOOLEAN
|
73
|
+
end
|
74
|
+
|
75
|
+
refine ::Set do
|
76
|
+
def dto_type
|
77
|
+
# TODO: Type checking of all values
|
78
|
+
Scheming::Type::Enum.new(
|
79
|
+
first.dto_type,
|
80
|
+
values: dup.freeze
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
using self
|
86
|
+
|
87
|
+
module_function
|
88
|
+
|
89
|
+
def resolve(any) = any.dto_type
|
90
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Type Specifications
|
4
|
+
#
|
5
|
+
# These helpers allow for a more ergonomic
|
6
|
+
# resolution of types
|
7
|
+
#
|
8
|
+
module Scheming::DSL::TypeSpecs
|
9
|
+
def Enum(*values) # rubocop:disable Naming/MethodName
|
10
|
+
Scheming::DSL::TypeResolver.resolve(values.to_set)
|
11
|
+
end
|
12
|
+
|
13
|
+
def Object(**attributes) # rubocop:disable Naming/MethodName
|
14
|
+
attrs = attributes.map do |field_name, type_spec|
|
15
|
+
Scheming::Attribute.new(
|
16
|
+
field_name:,
|
17
|
+
type: Scheming::DSL::TypeResolver.resolve(type_spec),
|
18
|
+
is_required: true
|
19
|
+
)
|
20
|
+
end
|
21
|
+
list = Scheming::Attribute::List.new(attrs)
|
22
|
+
Scheming::Type::Object.new(list)
|
23
|
+
end
|
24
|
+
|
25
|
+
def Nullable(type_spec) # rubocop:disable Naming/MethodName
|
26
|
+
type = Scheming::DSL::TypeResolver.resolve(type_spec)
|
27
|
+
Scheming::Type::Nullable.new(type)
|
28
|
+
end
|
29
|
+
end
|
data/lib/scheming/dsl.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Domain Specific Language
|
4
|
+
#
|
5
|
+
# Everyone loves magic; I myself enjoy dabling in
|
6
|
+
# the dark arts from time to time. Having said that,
|
7
|
+
# this magic is best if it's understandable for those
|
8
|
+
# who need to know.
|
9
|
+
#
|
10
|
+
module Scheming::DSL
|
11
|
+
require_relative 'dsl/type_specs'
|
12
|
+
require_relative 'dsl/data_builder'
|
13
|
+
require_relative 'dsl/object_type_def'
|
14
|
+
require_relative 'dsl/type_resolver'
|
15
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Scheming::Schema
|
4
|
+
# @private
|
5
|
+
module JSON
|
6
|
+
module Constants
|
7
|
+
NULL = { type: 'null' }.freeze
|
8
|
+
INTEGER = { type: 'integer' }.freeze
|
9
|
+
FLOAT = { type: 'numeric' }.freeze
|
10
|
+
STRING = { type: 'string' }.freeze
|
11
|
+
BOOLEAN = { type: 'boolean' }.freeze
|
12
|
+
end
|
13
|
+
private_constant :Constants
|
14
|
+
|
15
|
+
refine Scheming::Type::Object do
|
16
|
+
# @!attribute [r] attributes
|
17
|
+
# @return [Scheming::Attribute::List]
|
18
|
+
|
19
|
+
# @return [Hash]
|
20
|
+
def schema
|
21
|
+
{
|
22
|
+
type: 'object',
|
23
|
+
additionalProperties: false,
|
24
|
+
required:,
|
25
|
+
properties:
|
26
|
+
}.freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def required
|
32
|
+
attributes.required.map!(&:field_name).freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
def properties
|
36
|
+
attributes.to_h.transform_values do |attr|
|
37
|
+
attr.type.schema
|
38
|
+
end.freeze
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine Scheming::Type::Nullable do
|
43
|
+
# @!attribute [r] type
|
44
|
+
# @return [Scheming::Type::Base]
|
45
|
+
|
46
|
+
# @return [Hash]
|
47
|
+
def schema
|
48
|
+
{
|
49
|
+
oneOf: [
|
50
|
+
type.schema,
|
51
|
+
Constants::NULL
|
52
|
+
].freeze
|
53
|
+
}.freeze
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
refine Scheming::Type::Array do
|
58
|
+
# @!attribute [r] type
|
59
|
+
# @return [Scheming::Type::Base]
|
60
|
+
|
61
|
+
def schema
|
62
|
+
{
|
63
|
+
type: 'array',
|
64
|
+
items: type.schema
|
65
|
+
}.freeze
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
refine Scheming::Type::Enum do
|
70
|
+
def schema
|
71
|
+
type.schema.merge(enum: values).freeze
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
refine Scheming::Type::String do
|
76
|
+
# @return [Hash]
|
77
|
+
def schema = Constants::STRING
|
78
|
+
end
|
79
|
+
|
80
|
+
refine Scheming::Type::Integer do
|
81
|
+
# @return [Hash]
|
82
|
+
def schema = Constants::INTEGER
|
83
|
+
end
|
84
|
+
|
85
|
+
refine Scheming::Type::Float do
|
86
|
+
# @return [Hash]
|
87
|
+
def schema = Constants::FLOAT
|
88
|
+
end
|
89
|
+
|
90
|
+
refine Scheming::Type::Boolean do
|
91
|
+
# @return [Hash]
|
92
|
+
def schema = Constants::BOOLEAN
|
93
|
+
end
|
94
|
+
|
95
|
+
using self
|
96
|
+
|
97
|
+
module_function
|
98
|
+
|
99
|
+
# @param type [Scheming::Type::Base]
|
100
|
+
# @return [Hash]
|
101
|
+
def schema(type) = type.schema
|
102
|
+
end
|
103
|
+
private_constant :JSON
|
104
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Scheming
|
4
|
+
# = Schema
|
5
|
+
#
|
6
|
+
# Responsible for producing different schema
|
7
|
+
# formats which describe the data type provided
|
8
|
+
# to it.
|
9
|
+
#
|
10
|
+
module Schema
|
11
|
+
require_relative 'schema/json'
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
# @param raw_type [Scheming::Type::Base]
|
16
|
+
# @return [Hash]
|
17
|
+
def json(raw_type)
|
18
|
+
type = Scheming::DSL::TypeResolver.resolve(raw_type)
|
19
|
+
JSON.schema(type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# = Type
|
4
|
+
#
|
5
|
+
# Everything needed to describe and work
|
6
|
+
# with defining types to encode to.
|
7
|
+
#
|
8
|
+
module Scheming::Type
|
9
|
+
# = Base Type Definition
|
10
|
+
#
|
11
|
+
# Any and all shared functionality comes from
|
12
|
+
# this base type definition.
|
13
|
+
#
|
14
|
+
Base = Class.new
|
15
|
+
|
16
|
+
# = Object Type Definition
|
17
|
+
#
|
18
|
+
# Holds any number of named fields and the
|
19
|
+
# types that they hold
|
20
|
+
#
|
21
|
+
class Object < Base
|
22
|
+
# @return [Scheming::Attribute::List]
|
23
|
+
attr_reader :attributes
|
24
|
+
|
25
|
+
# @param attributes [Scheming::Attribute::List]
|
26
|
+
def initialize(attributes = Scheming::Attribute::List.empty)
|
27
|
+
super()
|
28
|
+
@attributes = attributes
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# = Nullable Type Definition
|
34
|
+
#
|
35
|
+
# Type wrapper that describes a type can be either
|
36
|
+
# the type provided OR it may be Null
|
37
|
+
#
|
38
|
+
class Nullable < Base
|
39
|
+
# @return [Scheming::Type::Base]
|
40
|
+
attr_reader :type
|
41
|
+
|
42
|
+
# @param type [Scheming::Type::Base]
|
43
|
+
def initialize(type)
|
44
|
+
super()
|
45
|
+
@type = type
|
46
|
+
freeze
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# = Enumeration Type Definition
|
51
|
+
#
|
52
|
+
# The wrapper that describes a type and holds
|
53
|
+
# a discrete set of values of that type.
|
54
|
+
#
|
55
|
+
class Enum < Base
|
56
|
+
# @return [Scheming::Type::Base]
|
57
|
+
attr_reader :type
|
58
|
+
|
59
|
+
# @return [Set<Object>]
|
60
|
+
attr_reader :values
|
61
|
+
|
62
|
+
# @param type [Scheming::Type::Base]
|
63
|
+
# @param values [Set<Object>]
|
64
|
+
def initialize(type, values:)
|
65
|
+
super()
|
66
|
+
@type = type
|
67
|
+
@values = values
|
68
|
+
freeze
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# = Array Type Definition
|
73
|
+
#
|
74
|
+
# Type wrapper which describes an array of zero
|
75
|
+
# or more of the provided type.
|
76
|
+
#
|
77
|
+
class Array < Base
|
78
|
+
# @return [Scheming::Type::Base]
|
79
|
+
attr_reader :type
|
80
|
+
|
81
|
+
# @param type [Scheming::Type::Base]
|
82
|
+
def initialize(type)
|
83
|
+
super()
|
84
|
+
@type = type
|
85
|
+
freeze
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# = Integer Type Definition
|
90
|
+
class Integer < Base; end
|
91
|
+
|
92
|
+
# = Float Type Definition
|
93
|
+
class Float < Base; end
|
94
|
+
|
95
|
+
# = String Type Definition
|
96
|
+
class String < Base; end
|
97
|
+
|
98
|
+
# = Boolean Type Definition
|
99
|
+
class Boolean < Base; end
|
100
|
+
end
|
data/lib/scheming.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'scheming/version'
|
4
|
+
|
5
|
+
# = Scheming
|
6
|
+
#
|
7
|
+
module Scheming
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
require_relative 'scheming/attribute'
|
11
|
+
require_relative 'scheming/type'
|
12
|
+
require_relative 'scheming/schema'
|
13
|
+
require_relative 'scheming/dsl'
|
14
|
+
|
15
|
+
# @return [Class]
|
16
|
+
def self.object(&)
|
17
|
+
builder = Scheming::DSL::DataBuilder.new
|
18
|
+
builder.instance_exec(&)
|
19
|
+
builder.build
|
20
|
+
end
|
21
|
+
end
|
data/sig/scheming.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scheming
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Falk
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ergonomic Data Design for the Masses
|
14
|
+
email:
|
15
|
+
- benjamin.falk@yahoo.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rspec"
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- Gemfile
|
24
|
+
- Gemfile.lock
|
25
|
+
- LICENSE.txt
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- lib/scheming.rb
|
29
|
+
- lib/scheming/attribute.rb
|
30
|
+
- lib/scheming/attribute/list.rb
|
31
|
+
- lib/scheming/attribute/list_builder.rb
|
32
|
+
- lib/scheming/dsl.rb
|
33
|
+
- lib/scheming/dsl/data_builder.rb
|
34
|
+
- lib/scheming/dsl/object_type_def.rb
|
35
|
+
- lib/scheming/dsl/type_resolver.rb
|
36
|
+
- lib/scheming/dsl/type_specs.rb
|
37
|
+
- lib/scheming/schema.rb
|
38
|
+
- lib/scheming/schema/json.rb
|
39
|
+
- lib/scheming/type.rb
|
40
|
+
- lib/scheming/version.rb
|
41
|
+
- sig/scheming.rbs
|
42
|
+
homepage:
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
rubygems_mfa_required: 'true'
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.2'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.4.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Designing Data
|
66
|
+
test_files: []
|