bed 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1377de7e50abf7f48aba8a104bc6fce6be53bc7b5fef46f18a08c34de88f12fb
4
- data.tar.gz: ef45d49fa33044142cf8eab1cc1a0caa2fa6f791467396509ac1746d2d96239c
3
+ metadata.gz: 685203c96b43ba149593ae12988d86524bdcaebed5b42f7c8ebd342a6c975b6a
4
+ data.tar.gz: 82052332d1e4d22f7b1aeaa40c6896f468b48f7efdac03001bce43c5f5cb7345
5
5
  SHA512:
6
- metadata.gz: 1ee08f53a9b7ccca782708880be79fb327832029e6e99942016d029446fa0310c7515998b3da521d03eed7ed48926ee577a25cfa0669cec0ed025f59fd44cee5
7
- data.tar.gz: 1d74e2f6cb14269c6164b54f95ca4c1bcb8a0893a7f080eedb9573c89fdbe5f1d0c139d991913b1f17f795787bd654f6fdd01b0d7c52a688f15ddfb6e40d21bd
6
+ metadata.gz: 57f45088cfd56960ee74a6e4519227e69d5c26d7f64a7a9e059e8cad0934a6d4039ffedd2673d5e7cd3aa1b37dd9d2d9dad3dfc9db54d2cd773c48a38e102e5f
7
+ data.tar.gz: 24ed22e1bfedb4b2155b56ec427300898160e79f9cb8daaa6a9176d2ae076ea5f0f3280ee85efcb55df78d46e6b4d856eda8afbd65a57cbf3ad8d993f64b134e
data/bed.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require_relative "lib/bed/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "bed"
6
+ spec.version = Bed::VERSION
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.authors = ["David Gillis"]
9
+ spec.email = ["david.gillis@hey.com"]
10
+ spec.summary = "A simple, modern schema library built on top of Data class"
11
+ spec.description = "Bed is a simple, modern schema library built on top of Data. It provides a simple, declarative way to define schemas for your data, and a way to validate that data against those schemas."
12
+ spec.required_ruby_version = ">= 3.0.0"
13
+ spec.homepage = "https://github.com/gillisd/bed"
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.license = "MIT"
16
+
17
+ spec.files = Dir.glob([
18
+ "lib/**/*",
19
+ "README.md",
20
+ "LICENSE.txt",
21
+ "*.gemspec"
22
+ ]).reject { |f| File.directory?(f) }
23
+
24
+ spec.bindir = "exe"
25
+ spec.executables = Dir.glob("exe/*").map { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "activesupport", ">= 6.0"
29
+ spec.add_dependency "json"
30
+ end
@@ -0,0 +1,23 @@
1
+ module Bed
2
+ class Builder
3
+ def initialize(schema, &block)
4
+ @schema = schema
5
+ end
6
+
7
+ def define_methods
8
+ @schema.deconstruct_keys(nil).each do |key, value|
9
+ case value
10
+ in Data
11
+ Bed::Builder.new()
12
+ end
13
+
14
+ end
15
+ end
16
+
17
+ def member?(name)
18
+ @schema.members.include?(name)
19
+ end
20
+
21
+
22
+ end
23
+ end
data/lib/bed/caster.rb ADDED
@@ -0,0 +1,46 @@
1
+ module Bed
2
+ class Caster
3
+ def self.cast(buildable)
4
+ new(buildable).cast
5
+ end
6
+
7
+ def initialize(buildable)
8
+ @buildable = buildable
9
+ end
10
+
11
+ def cast
12
+ raise TypeError, "#{@buildable} does not respond to to_h" unless @buildable.respond_to?(:to_h)
13
+
14
+ attributes = @buildable.to_h.transform_values do |value|
15
+ case value
16
+ in Hash
17
+ self.class.cast(value)
18
+ in Array
19
+ value.map do |v|
20
+ begin
21
+ self.class.cast(v)
22
+ rescue TypeError
23
+ v
24
+ end
25
+ end
26
+ else
27
+ value
28
+ end
29
+ end
30
+
31
+ cache_key = [attributes.keys]
32
+
33
+ Thread.current[:__bed_cast_cache] ||= {}
34
+ Thread.current[:__bed_cast_cache][cache_key] ||= (
35
+ Data.define(*attributes.keys, &method(:definition))
36
+ )
37
+ Thread.current[:__bed_cast_cache][cache_key].new(**attributes)
38
+ end
39
+
40
+ private
41
+
42
+ def definition(instance)
43
+ instance
44
+ end
45
+ end
46
+ end
data/lib/bed/data.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Bed
2
+ if defined?(::Data)
3
+ Data = ::Data
4
+ else
5
+ Data = ::Struct
6
+ Data.class_eval do
7
+ def self.define(*args, &block)
8
+ new(*args, &block)
9
+ end
10
+
11
+ def []=(key, value)
12
+ self[key] = value
13
+ end
14
+
15
+ def to_data
16
+ self
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,139 @@
1
+ class Object
2
+ class << self
3
+ alias_method :original_method_missing, :method_missing
4
+
5
+ def method_missing(method_name, *args, &block)
6
+ if method_name == :String
7
+ puts "Intercepted call to String with argument: #{args.first}"
8
+ define_method(args.first) do
9
+ "This is a dynamically defined method: #{args.first}"
10
+ end
11
+ else
12
+ original_method_missing(method_name, *args, &block)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ module Bed
19
+ class Schema
20
+ attr_reader :fields
21
+
22
+ def initialize(fields)
23
+ @fields = fields
24
+ end
25
+
26
+ def pattern
27
+ fields.map { |_, v| v.is_a?(Schema) ? v.pattern : '_' }.join(', ')
28
+ end
29
+ end
30
+
31
+ class Type
32
+ class << self
33
+ attr_reader :schema
34
+
35
+ def define(schema)
36
+ @schema = schema
37
+ Data.define(*schema.fields.keys) do
38
+
39
+ def initialize(...)
40
+ super
41
+ validate
42
+ end
43
+
44
+ define_method(:deconstruct) do
45
+ self.class.schema.fields.keys.map do |key|
46
+ value = public_send(key)
47
+ if value.is_a?(Data) && value.class.respond_to?(:schema)
48
+ value.deconstruct
49
+ else
50
+ value
51
+ end
52
+ end
53
+ end
54
+
55
+ define_method(:deconstruct_keys) do |keys = nil|
56
+ keys ||= self.class.schema.fields.keys
57
+ keys.each_with_object({}) do |key, hash|
58
+ if self.class.schema.fields.key?(key)
59
+ value = public_send(key)
60
+ hash[key] = if value.is_a?(Data) && value.class.respond_to?(:schema)
61
+ value.deconstruct_keys
62
+ else
63
+ value
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ define_method(:to_hash) do
70
+ self.class.schema.fields.keys.each_with_object({}) do |key, hash|
71
+ value = public_send(key)
72
+ hash[key] = if value.is_a?(Data) && value.class.respond_to?(:schema)
73
+ value.to_hash
74
+ else
75
+ value
76
+ end
77
+ end
78
+ end
79
+
80
+ define_method(:validate) do
81
+ pattern = "case self\nin [#{self.class.schema.pattern}]\ntrue\nelse\nfalse\nend"
82
+ eval(pattern)
83
+ end
84
+
85
+ # Class variable to store the schema
86
+ class_variable_set(:@@schema, schema)
87
+
88
+ # Class method to access the schema
89
+ define_singleton_method(:schema) do
90
+ class_variable_get(:@@schema)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Field = Data.define(:name, :type, :required, :enable_default, :default_value, :allow_nil) do
98
+ def initialize(name:, type:, required: true, enable_default: false, default_value: nil, allow_nil: false)
99
+ super
100
+ end
101
+ end
102
+
103
+ class SchemaBuilder
104
+ attr_reader :fields
105
+
106
+ def initialize
107
+ @fields = {}
108
+ end
109
+
110
+ Object.constants.each do |const|
111
+ define_method(const) do |field_name, **args|
112
+ define_field(Object.const_get(const), field_name, **args)
113
+ end
114
+ end
115
+
116
+ def compile(&block)
117
+ instance_eval(&block)
118
+ end
119
+
120
+ def to_schema
121
+ Schema.new(@fields)
122
+ end
123
+
124
+ def define_field(type, field_name, required: true, enable_default: false, default_value: nil, allow_nil: false)
125
+ @fields[field_name] = Field.new(name: field_name, type: type, required: required, enable_default: enable_default, default_value: default_value, allow_nil: allow_nil)
126
+ end
127
+
128
+ private
129
+
130
+ def const_missing(type)
131
+ Object.const_get(type)
132
+ end
133
+
134
+ def method_missing(type, *args)
135
+ field_name = args.first
136
+ @fields[field_name] = Object.const_get(type)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,157 @@
1
+ module Bed
2
+ module Flex
3
+ class Builder
4
+ def initialize(*args, **kwargs, &blk)
5
+ @whitelisted_attributes = []
6
+ @attributes = {}
7
+ @locked = false
8
+ @converged = false
9
+ result = if block_given?
10
+ block = blk
11
+ if block.arity.zero?
12
+ instance_eval(&block)
13
+ else
14
+ yield self
15
+ end
16
+ end
17
+
18
+ converge(result) if should_converge?(result)
19
+ @locked = true
20
+ end
21
+
22
+ def converge(value)
23
+ @converged = true
24
+ @attributes[:_converged] = value
25
+ end
26
+
27
+ def get(path)
28
+ path.to_s.split('.').inject(self) { |obj_self, attr| obj_self.public_send(attr) }
29
+ end
30
+
31
+ def to_h
32
+ return @attributes[:_converged] if @converged
33
+ @attributes.transform_values { |v| v.is_a?(Builder) ? v.to_h : v }
34
+ end
35
+
36
+ # alias_method :inspect, :to_h
37
+ alias_method :to_s, :to_h
38
+
39
+ def deep_dup
40
+ self.class.new.tap do |new_builder|
41
+ @attributes.each do |key, value|
42
+ new_builder.send(:"#{key}=", value.is_a?(Builder) ? value.deep_dup : value.dup)
43
+ end
44
+ end
45
+ end
46
+
47
+ def pretty_print(pp)
48
+ pp.object_address_group(self) do
49
+ pp.seplist(@attributes, -> { pp.text ',' }) do |k, v|
50
+ pp.breakable
51
+ pp.group(1) do
52
+ pp.text k.to_s
53
+ pp.text ':'
54
+ pp.breakable
55
+ pp.pp v
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ private alias_method :old_inspect, :inspect
62
+
63
+ def inspect
64
+ case @attributes
65
+ in {} then '{}'
66
+ in { _converged: value } then value
67
+ else
68
+ old_inspect
69
+ end
70
+ end
71
+
72
+ # def to_ary
73
+ # [to_h]
74
+ # end
75
+ def []=(name, value)
76
+ set_attribute(name, value)
77
+ end
78
+
79
+ private
80
+
81
+ def should_converge?(result)
82
+ case [@attributes, result]
83
+ # E.g.
84
+ # b.foo do
85
+ # b.bar 'a string'
86
+ # end
87
+ in {}, Builder | nil then false
88
+ # E.g.
89
+ # b.foo do
90
+ # 'a string'
91
+ # end
92
+ in {}, _ then true
93
+ else false
94
+ end
95
+ end
96
+
97
+ def respond_to_missing?(name, include_private = false)
98
+ return false if name == :to_ary
99
+ @whitelisted_attributes.empty? || @whitelisted_attributes.include?(name) || super
100
+ end
101
+
102
+ def method_missing(name, *args, &block)
103
+ return super if !@whitelisted_attributes.empty? && !@whitelisted_attributes.include?(name)
104
+
105
+ if name.end_with?('=')
106
+ attribute_name = name.to_s.chomp('=').to_sym
107
+ set_attribute(attribute_name, args.first)
108
+ else
109
+ get_or_set_attribute(name, *args, &block)
110
+ end
111
+ end
112
+
113
+ def to_ary
114
+ [inspect]
115
+ end
116
+
117
+ def get_or_set_attribute(name, *args, &block)
118
+ if args.empty? && !block
119
+ value = @attributes[name]
120
+
121
+ if value.nil? && @locked
122
+ raise NoMethodError, "#{name} is not defined"
123
+ end
124
+ else
125
+ value = if block
126
+ if block.arity == 0
127
+ args.size > 0 ? [*args,->{Builder.new(&block)}] : Builder.new(&block)
128
+ else
129
+ Builder.new.tap { |b| block.call(b) }
130
+ end
131
+ else
132
+ args.size == 1 ? args.first : args
133
+ end
134
+ set_attribute(name, value)
135
+ end
136
+ end
137
+
138
+ def set_attribute(name, value)
139
+ @attributes[name.to_sym] = value
140
+
141
+ define_attribute_method(name)
142
+ self
143
+ end
144
+
145
+ def define_attribute_method(name)
146
+ singleton_class.define_method(name) do |*args, &block|
147
+ if args.empty? && !block
148
+ name.to_s
149
+ @attributes[name]
150
+ else
151
+ get_or_set_attribute(name, *args, &block)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
data/lib/bed/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Bed
4
- VERSION = "0.1.0"
3
+ module Bed
4
+ VERSION = '0.2.1'
5
5
  end
data/lib/bed.rb CHANGED
@@ -1,9 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bed/schema'
3
+ require 'json'
4
+ require 'active_support/all'
5
+ require_relative 'bed/data'
6
+ require_relative 'bed/definition'
7
+ require_relative 'bed/caster'
8
+ require_relative 'bed/flex/builder'
4
9
  require_relative "bed/version"
5
10
 
6
- class Bed
11
+ module Bed
7
12
  class Error < StandardError; end
8
- # Your code goes here...
13
+
14
+ def self.define(**kwargs)
15
+ schema = Schema.new(kwargs)
16
+ Type.define(schema)
17
+ end
18
+
19
+ def self.schema(&block)
20
+ if block_given?
21
+ builder = SchemaBuilder.new
22
+ builder.compile(&block)
23
+ schema = builder.to_schema
24
+ define(**schema.fields)
25
+ else
26
+ self
27
+ end
28
+ end
29
+
30
+ def self.flex(&block)
31
+ buildable = Flex::Builder.new(&block)
32
+ Caster.cast(buildable)
33
+ end
34
+
35
+ def self.infer_file(pathname)
36
+ infer(pathname)
37
+ end
38
+
39
+ def self.infer(inferrable)
40
+ buildable = get_buildable(inferrable)
41
+
42
+ Caster.cast(buildable)
43
+ end
44
+
45
+ def self.get_buildable(inferrable)
46
+ if looks_like_json?(inferrable)
47
+ return JSON.parse(inferrable, symbolize_names: true)
48
+ end
49
+
50
+ case inferrable
51
+ in String
52
+ JSON.load_file(inferrable, symbolize_names: true)
53
+ in Hash
54
+ inferrable
55
+ else
56
+ raise ArgumentError, "inferrable must be a String, Hash, JSON file path, or JSON string"
57
+ end
58
+ end
59
+
60
+ def self.looks_like_json?(str)
61
+ str.start_with?('{') || str.start_with?('[')
62
+ end
9
63
  end
metadata CHANGED
@@ -1,15 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-08-26 00:00:00.000000000 Z
12
- dependencies: []
10
+ date: 2024-12-22 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
13
40
  description: Bed is a simple, modern schema library built on top of Data. It provides
14
41
  a simple, declarative way to define schemas for your data, and a way to validate
15
42
  that data against those schemas.
@@ -19,20 +46,21 @@ executables: []
19
46
  extensions: []
20
47
  extra_rdoc_files: []
21
48
  files:
22
- - CODE_OF_CONDUCT.md
23
49
  - LICENSE.txt
24
50
  - README.md
25
- - Rakefile
51
+ - bed.gemspec
26
52
  - lib/bed.rb
27
- - lib/bed/schema.rb
53
+ - lib/bed/builder.rb
54
+ - lib/bed/caster.rb
55
+ - lib/bed/data.rb
56
+ - lib/bed/definition.rb
57
+ - lib/bed/flex/builder.rb
28
58
  - lib/bed/version.rb
29
- - sig/bed.rbs
30
59
  homepage: https://github.com/gillisd/bed
31
60
  licenses:
32
61
  - MIT
33
62
  metadata:
34
63
  homepage_uri: https://github.com/gillisd/bed
35
- post_install_message:
36
64
  rdoc_options: []
37
65
  require_paths:
38
66
  - lib
@@ -47,8 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
75
  - !ruby/object:Gem::Version
48
76
  version: '0'
49
77
  requirements: []
50
- rubygems_version: 3.5.11
51
- signing_key:
78
+ rubygems_version: 3.6.0.dev
52
79
  specification_version: 4
53
80
  summary: A simple, modern schema library built on top of Data class
54
81
  test_files: []
data/CODE_OF_CONDUCT.md DELETED
@@ -1,47 +0,0 @@
1
- # Code of Conduct
2
-
3
- ## Our Commitment
4
-
5
- We are committed to fostering a respectful and productive environment for all project contributors.
6
-
7
- ## Expected Behavior
8
-
9
- Contributors are expected to:
10
-
11
- - Treat others with respect and professionalism
12
- - Provide and accept constructive feedback graciously
13
- - Own their actions and decisions
14
- - Prioritize the needs of the project
15
- - Keep discussions and contributions focused on the project or task at hand
16
-
17
- ## Unacceptable Behavior
18
-
19
- The following behaviors are considered unacceptable:
20
-
21
- - Harassment or discrimination of any kind
22
- - Offensive comments or personal attacks
23
- - Sharing others' private information without consent
24
- - Any conduct deemed inappropriate in a professional setting
25
- - Engaging in political discussions
26
- - Integrating personal political views into contributions
27
-
28
- ## Enforcement
29
-
30
- Project maintainers are responsible for enforcing these standards. They may take appropriate action in response to any
31
- behavior that violates this Code of Conduct, including temporary or permanent bans from project communication channels
32
- or from contributing to the project itself.
33
-
34
- ## Reporting Violations
35
-
36
- Violations can be reported to the repository owner or project maintainers.
37
-
38
- All reports will be reviewed promptly and treated confidentially.
39
-
40
- ## Scope
41
-
42
- This Code of Conduct applies to all communication channels related to the project and when representing the project in
43
- public spaces.
44
-
45
- ## Attribution
46
-
47
- This Code of Conduct is adapted from the Contributor Covenant, version 2.1.
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- Minitest::TestTask.create
7
-
8
- task default: :test
data/lib/bed/schema.rb DELETED
@@ -1,99 +0,0 @@
1
- class Bed::Schema < Data
2
- def self.define_with_types(**kwargs)
3
- puts 'defining...'
4
- old_kwdargs = kwargs
5
- kwargs = kwargs.transform_values do |v|
6
- v.superclass == Data ? v.new : v
7
- end
8
-
9
- keys = kwargs.keys
10
-
11
- puts kwargs
12
-
13
- init_block = Proc.new do
14
- define_method :initialize do |args = nil|
15
- case args
16
- in Hash
17
- super(**kwargs.merge(args))
18
- in Array
19
- super(kwargs.merge(args))
20
- else
21
- super(**kwargs)
22
- end
23
-
24
- unless args.nil?
25
- # raise ArgumentError, 'Invalid arguments' unless validate
26
- end
27
- end
28
-
29
- alias_method :_deconstruct, :deconstruct
30
-
31
- define_method :deconstruct do
32
- puts 'deconstructing...'
33
- val = _deconstruct
34
- case val
35
- in *beginning, Class => data, *ending
36
- [*beginning, *data.deconstruct, *ending]
37
- in [*rest, Data => data]
38
- [*rest, *data.deconstruct]
39
- in [Data => data, *rest]
40
- [*data.deconstruct, *rest]
41
- else
42
- _deconstruct
43
- end
44
- end
45
-
46
- alias_method :_deconstruct_keys, :deconstruct_keys
47
-
48
- define_method :deconstruct_keys do |keys = self.class.schema.keys|
49
- puts keys
50
- _deconstruct_keys(keys)
51
- # _deconstruct_keys(keys).transform_values do |v|
52
- # v.respond_to?(:deconstruct_keys) ? v.deconstruct_keys(keys) : v
53
- # end
54
- end
55
-
56
- define_method :to_hash do
57
- to_h.transform_values do |v|
58
- v.respond_to?(:to_hash) ? v.to_hash : v
59
- end
60
- end
61
-
62
- define_method :validate do
63
- arr = self.class.new.deconstruct
64
- pattern = <<-RUBY
65
- case self
66
- in #{arr}
67
- true
68
- else
69
- false
70
- end
71
- RUBY
72
- eval(pattern)
73
- end
74
-
75
- define_singleton_method :schema do
76
- old_kwdargs
77
- end
78
-
79
- define_singleton_method :create_nested_objects do |grouped_data|
80
- grouped_data.to_a.reverse.reduce(nil) do |inner, (klass, values)|
81
- args = values.map(&:first)
82
- args << inner if inner
83
- klass.new(*args)
84
- end
85
- end
86
-
87
- define_singleton_method :go do
88
- schema.values.map { |v| v.respond_to?(:superclass) && v.superclass == Data ? v.go : self }.flatten
89
- end
90
-
91
- define_singleton_method :reconstruct do |*args|
92
- grouped = args.zip(go).group_by(&:last)
93
-
94
- create_nested_objects(grouped)
95
- end
96
- end
97
- Data.define(*keys, &init_block)
98
- end
99
- end
data/sig/bed.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Bed
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end