data_model 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6f87b58a7e7706f284041ccc8479520af309c3e632ef89311be431a11112273
4
+ data.tar.gz: e945d11a1ac73459be8b1c067eb8bc40cafe7f43a665cb069fecf880d413d91d
5
+ SHA512:
6
+ metadata.gz: f6470deed88de3b60a5aad2e5926724f4d3255567d0668016ff6c13e1a82608ea71dc8b82ae82a4b0993771c222ad3a9353236243e6c0e8f8c4c0423614e8c71
7
+ data.tar.gz: 82434cccbdb292e7e416e46eb1eb353193ff39329008a107b2837d97b2c907228c79c62f3be16a2f5966c437c777372f2c08708e0acf5b688fb2f22232565dae
data/.editorconfig ADDED
@@ -0,0 +1,10 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = tab
5
+ indent_size = 2
6
+
7
+ [*.yml]
8
+ indent_style = space
9
+ indent_size = 2
10
+
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_from: "https://raw.githubusercontent.com/mbriggs/configs/main/dotfiles/rubocop.yml"
2
+
3
+ Style/SymbolArray:
4
+ EnforcedStyle: brackets
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,80 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ data_model (0.0.1)
5
+ zeitwerk (~> 2.6)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ backport (1.2.0)
12
+ benchmark (0.2.0)
13
+ diff-lcs (1.5.0)
14
+ e2mmap (0.1.0)
15
+ jaro_winkler (1.5.4)
16
+ json (2.6.2)
17
+ kramdown (2.4.0)
18
+ rexml
19
+ kramdown-parser-gfm (1.1.0)
20
+ kramdown (~> 2.0)
21
+ mini_portile2 (2.8.0)
22
+ nokogiri (1.13.8)
23
+ mini_portile2 (~> 2.8.0)
24
+ racc (~> 1.4)
25
+ parallel (1.22.1)
26
+ parser (3.1.2.1)
27
+ ast (~> 2.4.1)
28
+ racc (1.6.0)
29
+ rainbow (3.1.1)
30
+ regexp_parser (2.6.0)
31
+ reverse_markdown (2.1.1)
32
+ nokogiri
33
+ rexml (3.2.5)
34
+ rubocop (1.36.0)
35
+ json (~> 2.3)
36
+ parallel (~> 1.10)
37
+ parser (>= 3.1.2.1)
38
+ rainbow (>= 2.2.2, < 4.0)
39
+ regexp_parser (>= 1.8, < 3.0)
40
+ rexml (>= 3.2.5, < 4.0)
41
+ rubocop-ast (>= 1.20.1, < 2.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 1.4.0, < 3.0)
44
+ rubocop-ast (1.21.0)
45
+ parser (>= 3.1.1.0)
46
+ ruby-progressbar (1.11.0)
47
+ solargraph (0.47.2)
48
+ backport (~> 1.2)
49
+ benchmark
50
+ bundler (>= 1.17.2)
51
+ diff-lcs (~> 1.4)
52
+ e2mmap
53
+ jaro_winkler (~> 1.5)
54
+ kramdown (~> 2.3)
55
+ kramdown-parser-gfm (~> 1.1)
56
+ parser (~> 3.0)
57
+ reverse_markdown (>= 1.0.5, < 3)
58
+ rubocop (>= 0.52)
59
+ thor (~> 1.0)
60
+ tilt (~> 2.0)
61
+ yard (~> 0.9, >= 0.9.24)
62
+ sus (0.14.0)
63
+ thor (1.2.1)
64
+ tilt (2.0.11)
65
+ unicode-display_width (2.3.0)
66
+ webrick (1.7.0)
67
+ yard (0.9.28)
68
+ webrick (~> 1.7.0)
69
+ zeitwerk (2.6.1)
70
+
71
+ PLATFORMS
72
+ ruby
73
+
74
+ DEPENDENCIES
75
+ data_model!
76
+ solargraph
77
+ sus (~> 0.14)
78
+
79
+ BUNDLED WITH
80
+ 2.3.7
data/config/sus.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "pp"
2
+ require_relative "../lib/data_model"
@@ -0,0 +1,14 @@
1
+ module Schema
2
+ extend self
3
+
4
+ def string
5
+ [:string]
6
+ end
7
+
8
+ def contact
9
+ [:hash, { open?: false },
10
+ [[:first_name, :string],
11
+ [:last_name, :string],
12
+ [:email, :string]]]
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ module DataModel
2
+ module Model
3
+ extend self
4
+
5
+ def defaults
6
+ {
7
+ # name of validator if a child, and validating a named property
8
+ property: nil,
9
+
10
+ # context passed to read and write
11
+ config: {},
12
+ types: nil,
13
+ children: [],
14
+
15
+ # default writer
16
+ write: ->(val) { val }
17
+ }
18
+ end
19
+
20
+ def validate!(model)
21
+ model => {
22
+ property:, config:, types:,
23
+ read: Proc, write: Proc,
24
+ children: Array,
25
+ }
26
+ end
27
+
28
+ def read(model, data)
29
+ validate! model
30
+ invoke(model, :read, data)
31
+ end
32
+
33
+ def write(model, data)
34
+ validate! model
35
+ invoke(model, :write, data)
36
+ end
37
+
38
+ private
39
+
40
+ def invoke(model, action, data)
41
+ fn = model.fetch(action)
42
+
43
+ case fn.arity
44
+ when 1
45
+ fn.call(data)
46
+ when 2
47
+ ctx = model.slice(:config, :types, :children)
48
+ fn.call(data, ctx)
49
+ else
50
+ raise "expected an arity of 1 or 2, got: #{fn.arity}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ module DataModel
2
+ class Registry
3
+ def self.instance
4
+ @instance ||= new(defaults)
5
+ end
6
+
7
+ def self.register(name, type)
8
+ instance.register(name, type)
9
+ end
10
+
11
+ def self.defaults
12
+ {
13
+ string: Type.string,
14
+ hash: Type.hash
15
+ }
16
+ end
17
+
18
+ def initialize(*defaults)
19
+ @types = {}
20
+
21
+ defaults.each do |d|
22
+ d.each do |k, s|
23
+ register(k, s)
24
+ end
25
+ end
26
+ end
27
+
28
+ def register(name, type)
29
+ @types[name] = Model.defaults.merge(type)
30
+ end
31
+
32
+ def type?(name)
33
+ @types.key?(name)
34
+ end
35
+
36
+ def type(name, property = nil)
37
+ unless @types.key?(name)
38
+ raise "#{name} is not registered as a type"
39
+ end
40
+
41
+ @types.fetch(name).merge(property:)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ module DataModel
2
+ module Scanner
3
+ extend self
4
+
5
+ def scan(schema, registry = Registry.instance)
6
+ start = { validator: nil, property: nil, state: :start }
7
+ result = schema.each_with_index.reduce(start) do |context, (token, pos)|
8
+ context => {state:, validator:, property:}
9
+ peek = schema[pos + 1]
10
+
11
+ # detect named validators, a name could be any type so cannot be detected
12
+ # by ruby type and state. detect name by ensuring we are at the start
13
+ # of scanning, and the next element is a valid type.
14
+ if state == :start && registry.type?(peek)
15
+ context.merge(
16
+ property: token,
17
+ state: :named,
18
+ )
19
+ else
20
+ # with the special case of a name aside, we can detect the meaning
21
+ # of further tokens by their type and scanner state
22
+ case token
23
+ when Symbol
24
+ unless [:start, :named].include?(state)
25
+ raise "got a symbol at pos #{pos}, but validator already defined"
26
+ end
27
+
28
+ unless registry.type?(token)
29
+ raise "expected a type in pos #{pos}, but found #{token.inspect} which is not a registered type"
30
+ end
31
+
32
+ context.merge(
33
+ validator: registry.type(token, property),
34
+ state: :defined,
35
+ )
36
+
37
+ when Hash
38
+ unless state == :defined
39
+ raise "got a hash at pos #{pos}, but state is not :defined (#{state.inspect})"
40
+ end
41
+
42
+ context.merge(
43
+ validator: validator.merge(config: token),
44
+ state: :configured,
45
+ )
46
+
47
+ when Array
48
+ unless [:defined, :configured].include?(state)
49
+ raise "#{schema.inspect} at pos #{pos}: expected (String | Hash | Symbol | Array), got #{token.class.name}"
50
+ end
51
+
52
+ children = token.map { |s| scan(s, registry) }
53
+
54
+ context.merge(
55
+ validator: validator.merge(children:),
56
+ state: :complete,
57
+ )
58
+ else
59
+ raise "got token #{token.inspect} at position #{pos} which was unexpected given the scanner was in a state of #{state}"
60
+ end
61
+ end
62
+ end
63
+
64
+ result.fetch(:validator)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,48 @@
1
+ module DataModel
2
+ module Type
3
+ extend self
4
+
5
+ def string
6
+ {
7
+ read: lambda do |val|
8
+ err = {}
9
+
10
+ unless val.is_a? String
11
+ err[:root] = "#{val} is not a string, it is a #{val.class.name}"
12
+ end
13
+
14
+ [val, err]
15
+ end
16
+ }
17
+ end
18
+
19
+ def hash
20
+ {
21
+ read: lambda do |val, ctx|
22
+ err = {}
23
+ ctx => {config:, children:}
24
+
25
+ unless val.is_a? Hash
26
+ err[:root] = "#{val} is not a hash, it is a #{val.class.name}"
27
+ return [val, err]
28
+ end
29
+
30
+ # TODO: this needs to be wayyyy better
31
+ if !config[:open?] && (val.length > children.length)
32
+ err[:root] = "more elements found in closed hash then specified children"
33
+ end
34
+
35
+ children.each do |child|
36
+ child => {property:}
37
+ v, e = Model.read(child, val.fetch(property))
38
+
39
+ val = val.merge({ property => v })
40
+ err[property] = e
41
+ end
42
+
43
+ [val, err]
44
+ end
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module DataModel
2
+ VERSION = "0.0.1"
3
+ end
data/lib/data_model.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "zeitwerk"
2
+
3
+ loader = Zeitwerk::Loader.for_gem
4
+ loader.setup
5
+
6
+ module DataModel
7
+ extend self
8
+
9
+ def validate(schema, data, registry: Registry.instance)
10
+ _, err = read(schema, data, registry:)
11
+ err.empty? || err.values.all?(&:empty?)
12
+ end
13
+
14
+ def read(schema, data, registry: Registry.instance)
15
+ Model.read(compiled(schema, registry:), data)
16
+ end
17
+
18
+ def write(schema, data, registry: Registry.instance)
19
+ Model.write(compiled(schema, registry:), data)
20
+ end
21
+
22
+ def compiled(model, registry: Registry.instance)
23
+ if model in Array
24
+ model = Scanner.scan(model, registry)
25
+ end
26
+ model
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: data_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt Briggs
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: solargraph
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A framework for describing and validating data.
56
+ email:
57
+ - matt@mattbriggs.net
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".editorconfig"
63
+ - ".rubocop.yml"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - config/sus.rb
67
+ - fixtures/schema.rb
68
+ - lib/data_model.rb
69
+ - lib/data_model/model.rb
70
+ - lib/data_model/registry.rb
71
+ - lib/data_model/scanner.rb
72
+ - lib/data_model/type.rb
73
+ - lib/data_model/version.rb
74
+ homepage: https://github.com/mbriggs/data_model
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ homepage_uri: https://github.com/mbriggs/data_model
79
+ source_code_uri: https://github.com/mbriggs/data_model
80
+ changelog_uri: https://github.com/mbriggs/data_model/releases
81
+ funding_uri: https://github.com/sponsors/mbriggs
82
+ rubygems_mfa_required: 'true'
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '2.7'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.3.7
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Define a model for your data
102
+ test_files: []