dialekt 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/README.md +88 -0
- data/dialekt.gemspec +58 -0
- data/lib/dialekt.rb +25 -0
- data/lib/dialekt/basic_type_checker.rb +27 -0
- data/lib/dialekt/dsl.rb +32 -0
- data/lib/dialekt/model/basic_property.rb +115 -0
- data/lib/dialekt/model/map_property.rb +255 -0
- data/lib/dialekt/model/scalar_property.rb +70 -0
- data/lib/dialekt/model/set_property.rb +182 -0
- data/lib/dialekt/ruby_type_checker.rb +29 -0
- data/lib/dialekt/util/call_adapter.rb +57 -0
- data/lib/dialekt/util/call_signature.rb +69 -0
- data/lib/dialekt/util/core_extensions.rb +101 -0
- data/lib/dialekt/version.rb +5 -0
- metadata +270 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 292d8eb12e8d5c07a6d08fcfc9b52f35bee2bba6f654f5651a6cdad705ad95a6
|
4
|
+
data.tar.gz: ba4af05bf33624ee2c466900224f79e8afebe5cddb9f0e5511796211be042ef6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 527a36bf572db17d099d155833870ecaa605dba432144328fbd43fc9347163e40a77b88615df437c5cb3da468f7d968c0c0dcf6c7fcb46d1472818ca460edaeb
|
7
|
+
data.tar.gz: cda9eae770da3b8497c2a4e7d569c18c1e857b8beb1d963c5094750a24b8ead69e6d2feb29d4925791480127dc2e250b8e3e7f326d7f3f2c8b4dabb14ccaef0d
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# What is it?
|
2
|
+
|
3
|
+
With Dialekt you can easily define properties for DSL objects that support nice accessor methods, type checking and transformation.
|
4
|
+
|
5
|
+
Dialekt is based on [Docile], which is a great tool to create DSLs in Ruby. However, you will soon find yourself creating lots of repetetive code to implement your DSL accessors. Dialekt aims to simplify this task.
|
6
|
+
|
7
|
+
[Docile]: https://github.com/ms-ati/docile
|
8
|
+
|
9
|
+
## Example
|
10
|
+
|
11
|
+
Let's assume you want to create a build tool called Backscratcher (sort of a [small rake][Backscratcher] to scratch an itch :-) that uses a DSL for configuration. Your tool supports tasks and dependencies between tasks, and tasks can be grouped into namespaces. You start by creating a model and using Dialekt to define the properties.
|
12
|
+
|
13
|
+
[Backscratcher]: https://en.wikipedia.org/wiki/Backscratcher
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require "dialekt"
|
17
|
+
require "forwardable"
|
18
|
+
|
19
|
+
module Backscratcher
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
class Task
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
# Create a set property containing strings
|
26
|
+
dsl_set :dependencies, value_type: String
|
27
|
+
|
28
|
+
def initialize(name:)
|
29
|
+
@name = name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class FileTask < Task
|
34
|
+
end
|
35
|
+
|
36
|
+
class Namespace
|
37
|
+
attr_reader :name
|
38
|
+
|
39
|
+
# Create a tasks hash with string keys
|
40
|
+
dsl_map :tasks, key_type: String, value_type: Task do
|
41
|
+
# Create an accessor for task entries
|
42
|
+
entry :task, value_factory: ->(key:) { Task.new(name: key) }
|
43
|
+
# Create an accessor for file task entries
|
44
|
+
entry :file, value_type: FileTask, value_factory: ->(key:) { FileTask.new(name: key) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a namespace hash with string keys
|
48
|
+
dsl_map :namespaces, key_type: String, value_type: Namespace do
|
49
|
+
# Create an accessor for namespace entries
|
50
|
+
entry :namespace, value_factory: ->(key:) { Namespace.new(name: key) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(name:)
|
54
|
+
@name = name
|
55
|
+
@namespaces = {}
|
56
|
+
@tasks = {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Make some methods available in the root namespace for convenience
|
61
|
+
def_delegators :root, :namespace, :task, :file
|
62
|
+
|
63
|
+
def root
|
64
|
+
@root ||= Namespace.new(name: "")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
include Backscratcher
|
69
|
+
```
|
70
|
+
|
71
|
+
You now have a DSL for your build tool giving you methods to create namespaces, tasks and dependencies. Dialekt will handle the definition of accessores, type checking, creating new collection entries and applying DSL configurations:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
task "build" do
|
75
|
+
dependency "db:create"
|
76
|
+
dependency "db:load"
|
77
|
+
end
|
78
|
+
|
79
|
+
file "test.txt"
|
80
|
+
|
81
|
+
namespace "db" do
|
82
|
+
task "create"
|
83
|
+
|
84
|
+
task "load" do
|
85
|
+
dependencies ["db:create"]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
data/dialekt.gemspec
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
LIB_DIR = File.join(__dir__, "lib")
|
4
|
+
$LOAD_PATH.unshift(LIB_DIR) unless $LOAD_PATH.include?(LIB_DIR)
|
5
|
+
|
6
|
+
require "dialekt/version"
|
7
|
+
require "json"
|
8
|
+
require "pathname"
|
9
|
+
|
10
|
+
Gem::Specification.new do |spec|
|
11
|
+
raise "RubyGems 2.0 or newer is required." unless spec.respond_to?(:metadata)
|
12
|
+
|
13
|
+
spec.name = "dialekt"
|
14
|
+
spec.version = Dialekt::VERSION
|
15
|
+
spec.summary = "DSL utilities"
|
16
|
+
|
17
|
+
spec.required_ruby_version = ">= 2.6"
|
18
|
+
|
19
|
+
spec.authors = ["Jochen Seeber"]
|
20
|
+
spec.email = ["jochen@seeber.me"]
|
21
|
+
spec.homepage = "https://github.com/jochenseeber/dialekt"
|
22
|
+
|
23
|
+
spec.metadata["issue_tracker"] = "https://github.com/jochenseeber/dialekt/issues"
|
24
|
+
spec.metadata["documentation"] = "http://jochenseeber.github.com/dialekt"
|
25
|
+
spec.metadata["source_code"] = "https://github.com/jochenseeber/dialekt"
|
26
|
+
spec.metadata["wiki"] = "https://github.com/jochenseeber/dialekt/wiki"
|
27
|
+
|
28
|
+
spec.files = Dir[
|
29
|
+
"*.gemspec",
|
30
|
+
"*.md",
|
31
|
+
"*.txt",
|
32
|
+
"lib/**/*.rb",
|
33
|
+
]
|
34
|
+
|
35
|
+
spec.require_paths = [
|
36
|
+
"lib",
|
37
|
+
]
|
38
|
+
|
39
|
+
spec.bindir = "cmd"
|
40
|
+
spec.executables = spec.files.filter { |f| File.dirname(f) == "cmd" && File.file?(f) }.map { |f| File.basename(f) }
|
41
|
+
|
42
|
+
spec.add_dependency "docile", "~> 1.3.5"
|
43
|
+
spec.add_dependency "dry-inflector", "~> 0.2"
|
44
|
+
spec.add_dependency "zeitwerk", "~> 2.3"
|
45
|
+
|
46
|
+
spec.add_development_dependency "bundler", "~> 2.1"
|
47
|
+
spec.add_development_dependency "calificador", "~> 0.2.0"
|
48
|
+
spec.add_development_dependency "debase", "~> 0.2"
|
49
|
+
spec.add_development_dependency "minitest", "~> 5.14"
|
50
|
+
spec.add_development_dependency "qed", "~> 2.9"
|
51
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
52
|
+
spec.add_development_dependency "rubocop", "~> 1.6"
|
53
|
+
spec.add_development_dependency "rubocop-minitest", "~> 0.10"
|
54
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.5"
|
55
|
+
spec.add_development_dependency "ruby-debug-ide", "~> 0.7"
|
56
|
+
spec.add_development_dependency "simplecov", "~> 0.18"
|
57
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
58
|
+
end
|
data/lib/dialekt.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
require "zeitwerk"
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.setup
|
8
|
+
|
9
|
+
# Main module
|
10
|
+
module Dialekt
|
11
|
+
class Empty
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"<empty>"
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :inspect, :to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
EMPTY = Empty.instance
|
22
|
+
end
|
23
|
+
|
24
|
+
require "dialekt/dsl"
|
25
|
+
require "dialekt/util/core_extensions"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pp"
|
4
|
+
require "stringio"
|
5
|
+
|
6
|
+
module Dialekt
|
7
|
+
# Type checker
|
8
|
+
class BasicTypeChecker
|
9
|
+
def valid?(type:, value:)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def union_type(types:)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def check!(type:, value:)
|
18
|
+
raise TypeError, "Object must be of type(s) #{type}" unless valid?(type: type, value: value)
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(type:)
|
24
|
+
PP.singleline_pp(type, StringIO.new).string
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/dialekt/dsl.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialekt
|
4
|
+
# DSL extensions
|
5
|
+
module Dsl
|
6
|
+
# DSL mixins for Class
|
7
|
+
module ClassMixins
|
8
|
+
def dsl_scalar(name, **options, &block)
|
9
|
+
property = Model::ScalarProperty.new(name: name, **options)
|
10
|
+
Docile.dsl_eval(property, &block) if block
|
11
|
+
property.setup(owner: self)
|
12
|
+
property
|
13
|
+
end
|
14
|
+
|
15
|
+
def dsl_map(name, **options, &block)
|
16
|
+
property = Model::MapProperty.new(name: name, **options)
|
17
|
+
Docile.dsl_eval(property, &block) if block
|
18
|
+
property.setup(owner: self)
|
19
|
+
property
|
20
|
+
end
|
21
|
+
|
22
|
+
def dsl_set(name, **options, &block)
|
23
|
+
property = Model::SetProperty.new(name: name, **options)
|
24
|
+
Docile.dsl_eval(property, &block) if block
|
25
|
+
property.setup(owner: self)
|
26
|
+
property
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Class.include(ClassMixins)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialekt
|
4
|
+
module Model
|
5
|
+
# BaseAccessor base class
|
6
|
+
class BasicProperty
|
7
|
+
# Property configuration
|
8
|
+
class Shape
|
9
|
+
attr_reader :name, :type, :factory, :transformer
|
10
|
+
|
11
|
+
def initialize(name:, type:, factory: nil, transformer: nil)
|
12
|
+
@name = name.to_sym
|
13
|
+
@type = type
|
14
|
+
@factory = factory&.call_adapter
|
15
|
+
@transformer = transformer&.call_adapter
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :name
|
20
|
+
attr_writer :type
|
21
|
+
|
22
|
+
def initialize(name:, type: nil, factory: nil, transformer: nil)
|
23
|
+
raise ArgumentError, "Name must not be nil" if name.nil?
|
24
|
+
|
25
|
+
@name = name
|
26
|
+
@type = type
|
27
|
+
|
28
|
+
@variable = :"@#{name}"
|
29
|
+
@factory = factory&.call_adapter
|
30
|
+
@transformer = transformer&.call_adapter
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup(owner:); end
|
34
|
+
|
35
|
+
def access_value(shape:, target:, value: EMPTY)
|
36
|
+
if value.equal?(EMPTY)
|
37
|
+
get_value(shape: shape, target: target)
|
38
|
+
else
|
39
|
+
set_value(shape: shape, target: target, value: value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_value(shape:, target:)
|
44
|
+
if target.instance_variable_defined?(@variable)
|
45
|
+
target.instance_variable_get(@variable)
|
46
|
+
else
|
47
|
+
value = shape.factory&.call(object: target)
|
48
|
+
target.instance_variable_set(@variable, value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_value(shape:, target:, value:)
|
53
|
+
if shape.transformer
|
54
|
+
begin
|
55
|
+
value = shape.transformer.call(object: target, value: value)
|
56
|
+
rescue StandardError
|
57
|
+
raise TypeError, "Cannot transform value '#{value}' for property #{@name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
type_checker = target.class.dialekt_type_checker
|
62
|
+
|
63
|
+
begin
|
64
|
+
type_checker.check!(type: shape.type, value: value)
|
65
|
+
rescue StandardError
|
66
|
+
raise TypeError, <<~MSG
|
67
|
+
Value '#{value}' (#{value.class}) for property #{@name} must conform to #{type_checker.format(type: shape.type)}
|
68
|
+
MSG
|
69
|
+
end
|
70
|
+
|
71
|
+
target.instance_variable_set(@variable, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
def type(type = EMPTY)
|
75
|
+
type == EMPTY ? @type : (@type = type)
|
76
|
+
end
|
77
|
+
|
78
|
+
def factory(factory = EMPTY, &block)
|
79
|
+
if factory == EMPTY
|
80
|
+
if block
|
81
|
+
self.factory = block
|
82
|
+
else
|
83
|
+
@factory
|
84
|
+
end
|
85
|
+
else
|
86
|
+
raise ArgumentError, "Please provide either a factory proc or a block, not both" if block
|
87
|
+
|
88
|
+
self.factory = factory
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def factory=(factory)
|
93
|
+
@factory = factory&.call_adapter
|
94
|
+
end
|
95
|
+
|
96
|
+
def transformer(transformer = EMPTY, &block)
|
97
|
+
if transformer == EMPTY
|
98
|
+
if block
|
99
|
+
self.transformer = block
|
100
|
+
else
|
101
|
+
@transformer
|
102
|
+
end
|
103
|
+
else
|
104
|
+
raise ArgumentError, "Please provide either a transformer proc or a block, not both" if block
|
105
|
+
|
106
|
+
self.transformer = transformer
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def transformer=(transformer)
|
111
|
+
@transformer = transformer&.call_adapter
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "docile"
|
4
|
+
require "stringio"
|
5
|
+
|
6
|
+
module Dialekt
|
7
|
+
module Model
|
8
|
+
# Base class for DSL map accessors
|
9
|
+
class MapProperty < BasicProperty
|
10
|
+
# Entry configuration
|
11
|
+
class Entry
|
12
|
+
attr_reader :name, :key_type, :key_transformer, :value_type, :value_factory, :value_transformer
|
13
|
+
|
14
|
+
def initialize(name:, key_type:, value_type:, key_transformer: nil, value_factory: nil, value_transformer: nil)
|
15
|
+
@name = name.to_sym
|
16
|
+
@key_type = key_type
|
17
|
+
@key_transformer = key_transformer&.call_adapter
|
18
|
+
@value_type = value_type
|
19
|
+
@value_factory = value_factory&.call_adapter
|
20
|
+
@value_transformer = value_transformer&.call_adapter
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
result = StringIO.new
|
25
|
+
result << @name << " (" << self.class.base_name << ") {"
|
26
|
+
result << "key_type: " << @key_type.to_s
|
27
|
+
result << ", key_transformer: " << @key_transformer.source_info if @key_transformer
|
28
|
+
result << ", value_type: " << @value_type.to_s
|
29
|
+
result << ", value_factory: " << @value_factory.source_info if @value_factory
|
30
|
+
result << ", value_transformer: " << @value_transformer.source_info if @value_transformer
|
31
|
+
result << "}"
|
32
|
+
|
33
|
+
result.string
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(
|
38
|
+
name:,
|
39
|
+
key_type: nil,
|
40
|
+
value_type: nil,
|
41
|
+
type: Hash,
|
42
|
+
factory: -> { {} },
|
43
|
+
transformer: ->(value:) { value&.to_h }
|
44
|
+
)
|
45
|
+
super(
|
46
|
+
name: name,
|
47
|
+
type: type,
|
48
|
+
factory: factory,
|
49
|
+
transformer: transformer
|
50
|
+
)
|
51
|
+
|
52
|
+
@key_type = key_type
|
53
|
+
@key_transformer = nil
|
54
|
+
|
55
|
+
@value_type = value_type
|
56
|
+
@value_transformer = nil
|
57
|
+
@value_factory = nil
|
58
|
+
|
59
|
+
@entries = {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def entries
|
63
|
+
@entries.dup.freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
def entries=(entries)
|
67
|
+
case entries
|
68
|
+
when Hash
|
69
|
+
@entries = {}
|
70
|
+
|
71
|
+
entries.each do |name, entry|
|
72
|
+
if name != entry.name
|
73
|
+
raise ArgumentError, "Entry key '#{name}' does not match entry name for '#{entry.name}'"
|
74
|
+
end
|
75
|
+
|
76
|
+
define_entry(entry)
|
77
|
+
end
|
78
|
+
when Enumerable
|
79
|
+
@entries = {}
|
80
|
+
entries.each { |entry| define_entry(entry) }
|
81
|
+
else
|
82
|
+
raise ArgumentError, "Entries must be an Enumerable or a Hash"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def entry(name, key_type: nil, key_transformer: nil, value_type: nil, value_transformer: nil, value_factory: nil)
|
87
|
+
entry = Entry.new(
|
88
|
+
name: name.to_sym,
|
89
|
+
key_type: key_type || @key_type,
|
90
|
+
key_transformer: key_transformer || @key_transformer,
|
91
|
+
value_type: value_type || @value_type,
|
92
|
+
value_factory: value_factory || @value_factory,
|
93
|
+
value_transformer: value_transformer || @value_transformer
|
94
|
+
)
|
95
|
+
|
96
|
+
define_entry(entry)
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup(owner:)
|
100
|
+
super
|
101
|
+
|
102
|
+
property = self
|
103
|
+
|
104
|
+
if @entries.empty?
|
105
|
+
raise StandardError, "Please specify a key type or entries for property '#{@name}'" if @key_type.nil?
|
106
|
+
raise StandardError, "Please specify a value type or entries for property '#{@name}'" if @value_type.nil?
|
107
|
+
|
108
|
+
define_entry(Entry.new(name: owner.dialekt_inflector.singularize(@name), key_type: @key_type, value_type: @value_type))
|
109
|
+
end
|
110
|
+
|
111
|
+
type_checker = owner.class.dialekt_type_checker
|
112
|
+
|
113
|
+
@key_type ||= type_checker.union_type(types: @entries.values.map(&:key_type))
|
114
|
+
@value_type ||= type_checker.union_type(types: @entries.values.map(&:value_type))
|
115
|
+
|
116
|
+
owner.define_method(@name) do |value = EMPTY, &block|
|
117
|
+
value = property.access_value(shape: property.map_shape, target: self, value: value, &block)
|
118
|
+
value.dup.freeze
|
119
|
+
end
|
120
|
+
|
121
|
+
owner.define_method(:"#{@name}=") do |value|
|
122
|
+
property.set_value(shape: property.map_shape, target: self, value: value)
|
123
|
+
end
|
124
|
+
|
125
|
+
@entries.each_value do |entry|
|
126
|
+
owner.define_method(entry.name) do |key, value = EMPTY, &block|
|
127
|
+
property.access_entry(entry: entry, target: self, key: key, value: value, &block)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def map_shape
|
133
|
+
@map_shape ||= BasicProperty::Shape.new(
|
134
|
+
name: @name,
|
135
|
+
type: @type,
|
136
|
+
factory: @factory,
|
137
|
+
transformer: @transformer
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
def access_entry(entry:, target:, key:, value: EMPTY, &block)
|
142
|
+
value = if value == EMPTY
|
143
|
+
get_entry(entry: entry, target: target, key: key)
|
144
|
+
else
|
145
|
+
set_entry(entry: entry, target: target, key: key, value: value)
|
146
|
+
end
|
147
|
+
|
148
|
+
Docile.dsl_eval(value, &block) if !value.nil? && block
|
149
|
+
value
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_entry(entry:, target:, key:)
|
153
|
+
map = get_value(shape: map_shape, target: target)
|
154
|
+
|
155
|
+
if entry.key_transformer
|
156
|
+
begin
|
157
|
+
key = entry.key_transformer.call(object: target, key: key)
|
158
|
+
rescue StandardError
|
159
|
+
raise ArgumentError, "Cannot transform key '#{key}' for property '#{@name}' (#{entry.name})"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
if entry.value_factory
|
164
|
+
map.fetch(key) do
|
165
|
+
value = begin
|
166
|
+
entry.value_factory.call(object: target, key: key)
|
167
|
+
rescue StandardError
|
168
|
+
raise StandardError, "Cannot create entry for '#{key}' for property '#{@name}' (#{entry.name})"
|
169
|
+
end
|
170
|
+
|
171
|
+
map[key] = value
|
172
|
+
end
|
173
|
+
else
|
174
|
+
map.fetch(key) do
|
175
|
+
raise KeyError, "No value for key '#{key}' for property '#{@name}' (#{entry.name})"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def set_entry(entry:, target:, key:, value:)
|
181
|
+
map = get_value(shape: map_shape, target: target)
|
182
|
+
type_checker = target.class.dialekt_type_checker
|
183
|
+
|
184
|
+
if entry.key_transformer
|
185
|
+
begin
|
186
|
+
key = entry.key_transformer.call(object: target, key: key)
|
187
|
+
rescue StandardError
|
188
|
+
raise ArgumentError, "Cannot transform key '#{key}' for property '#{@name}' (#{entry.name})"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
unless type_checker.valid?(type: entry.key_type, value: key)
|
193
|
+
raise TypeError, "Illegal key type '#{key.class}' for property '#{@name}' (#{entry.name})"
|
194
|
+
end
|
195
|
+
|
196
|
+
if entry.value_transformer
|
197
|
+
begin
|
198
|
+
value = entry.value_transformer.call(object: target, key: key, value: value)
|
199
|
+
rescue StandardError
|
200
|
+
raise ArgumentError, "Cannot transform value '#{value}' for property '#{@name}' (#{entry.name})"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
unless type_checker.valid?(type: entry.value_type, value: value)
|
205
|
+
raise TypeError, "Illegal value type '#{value.class}' for property '#{@name}' (#{entry.name})"
|
206
|
+
end
|
207
|
+
|
208
|
+
map.store(key, value)
|
209
|
+
end
|
210
|
+
|
211
|
+
def key_type(type = EMPTY)
|
212
|
+
type == EMPTY ? @key_type : (@key_type = type)
|
213
|
+
end
|
214
|
+
|
215
|
+
def key_transformer(transformer = EMPTY)
|
216
|
+
transformer == EMPTY ? @key_transformer : (@key_transformer = transformer&.call_adapter)
|
217
|
+
end
|
218
|
+
|
219
|
+
def value_type(type = EMPTY)
|
220
|
+
type == EMPTY ? @value_type : (@value_type = type)
|
221
|
+
end
|
222
|
+
|
223
|
+
def value_transformer(transformer = EMPTY)
|
224
|
+
transformer == EMPTY ? @value_transformer : (@value_transformer = transformer&.call_adapter)
|
225
|
+
end
|
226
|
+
|
227
|
+
def value_factory(factory = EMPTY)
|
228
|
+
factory == EMPTY ? @value_factory : (@value_factory = factory&.call_adapter)
|
229
|
+
end
|
230
|
+
|
231
|
+
def to_s
|
232
|
+
result = StringIO.new
|
233
|
+
|
234
|
+
result << @name << " (" << self.class.base_name << ") {"
|
235
|
+
result << "type: " << @type
|
236
|
+
result << ", key_type: " << @key_type
|
237
|
+
result << ", value_type: " << @value_type
|
238
|
+
result << ", factory: " << @factory.source_info if @factory
|
239
|
+
result << ", transformer: " << @transformer.source_info if @transformer
|
240
|
+
result << ", entries: [" << @entries.values.map(&:name).join(", ") << "]"
|
241
|
+
result << "}"
|
242
|
+
|
243
|
+
result.string
|
244
|
+
end
|
245
|
+
|
246
|
+
protected
|
247
|
+
|
248
|
+
def define_entry(entry)
|
249
|
+
raise ArgumentError, "Entry '#{entry.name}' already exists for property '#{@name}'" if @entries.key?(entry.name)
|
250
|
+
|
251
|
+
@entries[entry.name] = entry
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|