dialekt 0.1.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 +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
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "docile"
|
4
|
+
|
5
|
+
module Dialekt
|
6
|
+
module Model
|
7
|
+
# Base class for primitive DSL properties
|
8
|
+
class ScalarProperty < BasicProperty
|
9
|
+
def initialize(name:, type: nil, factory: nil, transformer: nil)
|
10
|
+
super(name: name, type: type, factory: factory, transformer: transformer)
|
11
|
+
|
12
|
+
@shapes = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup(owner:)
|
16
|
+
super
|
17
|
+
|
18
|
+
raise ArgumentError, "Missing type for property #{name} of #{owner}" if @shapes.empty? && @type.nil?
|
19
|
+
|
20
|
+
unless @shapes.key?(name)
|
21
|
+
@shapes[name] = BasicProperty::Shape.new(
|
22
|
+
name: name,
|
23
|
+
type: @type || owner.class.type_checker.union_type(types: @shapes.values.map(&:type)),
|
24
|
+
factory: @factory,
|
25
|
+
transformer: @transformer
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
property = self
|
30
|
+
|
31
|
+
@shapes.each_value do |shape|
|
32
|
+
owner.define_method(shape.name) do |value = EMPTY, &block|
|
33
|
+
property.access_value(shape: shape, target: self, value: value, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
owner.define_method(:"#{shape.name}=") do |value|
|
37
|
+
property.set_value(shape: shape, target: self, value: value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def shape(name = nil, **options)
|
43
|
+
name = name&.to_sym || self.name
|
44
|
+
|
45
|
+
raise ArgumentError, "Property #{self.name} already has a shape called #{name}" if @shapes.key?(name)
|
46
|
+
|
47
|
+
options[:type] ||= @type
|
48
|
+
options[:factory] ||= @factory
|
49
|
+
options[:transformer] ||= @transformer
|
50
|
+
|
51
|
+
raise ArgumentError, "Missing shape for value #{name} of property #{self.name}" if options[:type].nil?
|
52
|
+
|
53
|
+
config = BasicProperty::Shape.new(name: name, **options)
|
54
|
+
@shapes[name] = config
|
55
|
+
end
|
56
|
+
|
57
|
+
def shapes
|
58
|
+
@shapes.dup.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
def shapes=(shapes)
|
62
|
+
shapes = shapes.values if shapes.is_a?(Hash)
|
63
|
+
|
64
|
+
raise ArgumentError, "Shapes must be an Enumerable" unless shapes.is_a?(Enumerable)
|
65
|
+
|
66
|
+
@shapes = shapes.map { |shape| [shape.name, shape] }.to_h
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "docile"
|
4
|
+
require "dry/inflector"
|
5
|
+
|
6
|
+
module Dialekt
|
7
|
+
module Model
|
8
|
+
# Base class for DSL set accessors
|
9
|
+
class SetProperty < BasicProperty
|
10
|
+
class Entry
|
11
|
+
attr_reader :name, :value_type, :value_transformer
|
12
|
+
|
13
|
+
def initialize(name:, value_type:, value_transformer: nil)
|
14
|
+
@name = name.to_sym
|
15
|
+
@value_type = value_type
|
16
|
+
@value_transformer = value_transformer&.call_adapter
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
result = StringIO.new
|
21
|
+
result << @name << " (" << self.class.base_name << ") {"
|
22
|
+
result << "value_type: " << @value_type.to_s
|
23
|
+
result << ", value_transformer: " << @value_transformer.source_info if @value_transformer
|
24
|
+
result << "}"
|
25
|
+
|
26
|
+
result.string
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
name:,
|
32
|
+
value_type: nil,
|
33
|
+
type: Set,
|
34
|
+
factory: -> { Set.new },
|
35
|
+
transformer: ->(value:) { value&.to_set }
|
36
|
+
)
|
37
|
+
super(
|
38
|
+
name: name,
|
39
|
+
type: type,
|
40
|
+
factory: factory,
|
41
|
+
transformer: transformer
|
42
|
+
)
|
43
|
+
|
44
|
+
@value_type = value_type
|
45
|
+
@value_transformer = nil
|
46
|
+
@entries = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def entries
|
50
|
+
@entries.dup.freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
def entries=(entries)
|
54
|
+
case entries
|
55
|
+
when Hash
|
56
|
+
@entries = {}
|
57
|
+
|
58
|
+
entries.each do |name, entry|
|
59
|
+
if name != entry.name
|
60
|
+
raise ArgumentError, "Entry key '#{name}' does not match entry name for '#{entry.name}'"
|
61
|
+
end
|
62
|
+
|
63
|
+
define_entry(entry)
|
64
|
+
end
|
65
|
+
when Enumerable
|
66
|
+
@entries = {}
|
67
|
+
entries.each { |entry| define_entry(entry) }
|
68
|
+
else
|
69
|
+
raise ArgumentError, "Entries must be an Enumerable or a Hash"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def entry(name, value_type: nil, value_transformer: nil)
|
74
|
+
entry = Entry.new(
|
75
|
+
name: name.to_sym,
|
76
|
+
value_type: value_type || @value_type,
|
77
|
+
value_transformer: value_transformer || @value_transformer
|
78
|
+
)
|
79
|
+
|
80
|
+
define_entry(entry)
|
81
|
+
end
|
82
|
+
|
83
|
+
def setup(owner:)
|
84
|
+
super
|
85
|
+
|
86
|
+
property = self
|
87
|
+
|
88
|
+
if @entries.empty?
|
89
|
+
raise StandardError, "Please specify a value type for property '#{@name}'" if @value_type.nil?
|
90
|
+
|
91
|
+
define_entry(Entry.new(name: owner.dialekt_inflector.singularize(@name), value_type: @value_type))
|
92
|
+
end
|
93
|
+
|
94
|
+
@value_type ||= owner.class.type_checker.union_type(types: @entries.values.map(&:value_type))
|
95
|
+
|
96
|
+
owner.define_method(@name) do |value = EMPTY, &block|
|
97
|
+
value = property.access_value(shape: property.set_shape, target: self, value: value, &block)
|
98
|
+
value.dup.freeze
|
99
|
+
end
|
100
|
+
|
101
|
+
owner.define_method(:"#{@name}=") do |value|
|
102
|
+
property.set_value(shape: property.set_shape, target: self, value: value)
|
103
|
+
end
|
104
|
+
|
105
|
+
@entries.each_value do |entry|
|
106
|
+
owner.define_method(entry.name) do |value, &block|
|
107
|
+
property.add_entry(entry: entry, target: self, value: value, &block)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_shape
|
113
|
+
@set_shape ||= BasicProperty::Shape.new(
|
114
|
+
name: @name,
|
115
|
+
type: @type,
|
116
|
+
factory: @factory,
|
117
|
+
transformer: @transformer
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_entry(entry:, target:, value:, &block)
|
122
|
+
set = get_value(shape: set_shape, target: target)
|
123
|
+
|
124
|
+
if entry.value_transformer
|
125
|
+
begin
|
126
|
+
value = entry.value_transformer.call(object: target, value: value)
|
127
|
+
rescue StandardError
|
128
|
+
raise ArgumentError, "Cannot transform value '#{value}' for property '#{@name}' (#{entry.name})"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
unless target.class.dialekt_type_checker.valid?(type: entry.value_type, value: value)
|
133
|
+
raise TypeError, "Illegal value type '#{value.class}' for property '#{@name}' (#{entry.name})"
|
134
|
+
end
|
135
|
+
|
136
|
+
set.add(value)
|
137
|
+
|
138
|
+
Docile.dsl_eval(value, &block) if !value.nil? && block
|
139
|
+
|
140
|
+
value
|
141
|
+
end
|
142
|
+
|
143
|
+
def value_type(type = EMPTY)
|
144
|
+
type == EMPTY ? @value_type : (@value_type = type)
|
145
|
+
end
|
146
|
+
|
147
|
+
def value_transformer(transformer = EMPTY)
|
148
|
+
transformer == EMPTY ? @value_transformer : (@value_transformer = transformer&.call_adapter)
|
149
|
+
end
|
150
|
+
|
151
|
+
def value_factory(factory = EMPTY)
|
152
|
+
factory == EMPTY ? @value_factory : (@value_factory = factory&.call_adapter)
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_s
|
156
|
+
result = StringIO.new
|
157
|
+
|
158
|
+
result << @name << " (" << self.class.base_name << ") {"
|
159
|
+
result << "type: " << @type
|
160
|
+
result << ", value_type: " << @value_type
|
161
|
+
result << ", factory: " << @factory.source_info if @factory
|
162
|
+
result << ", transformer: " << @transformer.source_info if @transformer
|
163
|
+
result << ", entries: [" << @entries.values.map(&:name).join(", ") << "]"
|
164
|
+
result << "}"
|
165
|
+
|
166
|
+
result.string
|
167
|
+
end
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
def define_entry(entry)
|
172
|
+
if entry.name == @name
|
173
|
+
raise ArgumentError, "Entry '#{entry.name}' cannot have the same name as its set property"
|
174
|
+
end
|
175
|
+
|
176
|
+
raise ArgumentError, "Entry '#{entry.name}' already exists for property '#{@name}'" if @entries.key?(entry.name)
|
177
|
+
|
178
|
+
@entries[entry.name] = entry
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Dialekt
|
6
|
+
# Ruby type checker
|
7
|
+
class RubyTypeChecker < BasicTypeChecker
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def union_type(types:)
|
11
|
+
union_type = Set.new(types.flatten.uniq)
|
12
|
+
|
13
|
+
raise ArgumentError, "Types must not be empty" if union_type.empty?
|
14
|
+
|
15
|
+
union_type.size == 1 ? union_type.first : union_type
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid?(type:, value:)
|
19
|
+
case type
|
20
|
+
when Array, Set
|
21
|
+
type.any? { |t| valid?(type: t, value: value) }
|
22
|
+
when Class
|
23
|
+
value.is_a?(type)
|
24
|
+
else
|
25
|
+
raise TypeError, "Illegal type '#{type}'"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialekt
|
4
|
+
module Util
|
5
|
+
# Call adapter for Proc that filters out excess named parameters
|
6
|
+
class CallAdapter
|
7
|
+
def initialize(callable:)
|
8
|
+
@callable = callable
|
9
|
+
@signature = callable.call_signature
|
10
|
+
|
11
|
+
if @signature.required_parameter_count.positive?
|
12
|
+
raise ArgumentError, "Callable '#{callable}' must not have any required positional parameters"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method, *arguments, &block)
|
17
|
+
if method == :call
|
18
|
+
options = arguments.last&.keys || []
|
19
|
+
define_call_method(options: options)
|
20
|
+
send(method, *arguments, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
ruby2_keywords :method_missing
|
27
|
+
|
28
|
+
def respond_to_missing?(method, include_all = true)
|
29
|
+
method == :call ? true : super
|
30
|
+
end
|
31
|
+
|
32
|
+
def define_call_method(options:)
|
33
|
+
accepted_options = options.intersection(@signature.options.keys)
|
34
|
+
|
35
|
+
if accepted_options.size == options.size
|
36
|
+
define_singleton_method(:call, @callable)
|
37
|
+
elsif accepted_options.empty?
|
38
|
+
define_singleton_method(:call) do |**_call_options|
|
39
|
+
@callable.call
|
40
|
+
end
|
41
|
+
else
|
42
|
+
define_singleton_method(:call) do |**call_options|
|
43
|
+
@callable.call(**call_options.slice(*accepted_options))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call_adapter
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def source_info
|
53
|
+
"#{File.basename(@callable.source_location.first)}:#{@callable.source_location.last}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialekt
|
4
|
+
module Util
|
5
|
+
# Call signature information for Proc objects
|
6
|
+
class CallSignature
|
7
|
+
# Parameter information
|
8
|
+
class Parameter
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name:, optional:)
|
12
|
+
@name = name.to_sym
|
13
|
+
@optional = optional
|
14
|
+
end
|
15
|
+
|
16
|
+
def optional?
|
17
|
+
@optional
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
@name == other.name && @optional == other.optional?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def create(signature:)
|
27
|
+
parameters = []
|
28
|
+
options = {}
|
29
|
+
extra_parameters = nil
|
30
|
+
extra_options = nil
|
31
|
+
|
32
|
+
signature.each do |type, name|
|
33
|
+
case type
|
34
|
+
when :req, :opt
|
35
|
+
parameters << Parameter.new(name: name, optional: type == :opt)
|
36
|
+
when :rest
|
37
|
+
extra_parameters = Parameter.new(name: name, optional: true)
|
38
|
+
when :keyreq, :key
|
39
|
+
options[name] = Parameter.new(name: name, optional: type == :key)
|
40
|
+
when :keyrest
|
41
|
+
extra_options = Parameter.new(name: name, optional: true)
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Illegal type #{type} in signature #{PP.singleline_pp(signature, StringIO.new).string}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
new(parameters: parameters, extra_parameters: extra_parameters, options: options, extra_options: extra_options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :parameters, :options, :extra_parameters, :extra_options
|
52
|
+
|
53
|
+
def initialize(parameters:, extra_parameters:, options:, extra_options:)
|
54
|
+
@parameters = parameters.dup.freeze
|
55
|
+
@extra_parameters = extra_parameters
|
56
|
+
@options = options.dup.freeze
|
57
|
+
@extra_options = extra_options
|
58
|
+
end
|
59
|
+
|
60
|
+
def required_parameter_count
|
61
|
+
@required_parameter_count ||= @parameters.count { |p| !p.optional? }
|
62
|
+
end
|
63
|
+
|
64
|
+
def optional_parameter_count
|
65
|
+
@optional_parameter_count ||= @parameters.count(&:optional?)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialekt
|
4
|
+
module Util
|
5
|
+
# Core Ruby extensions
|
6
|
+
module CoreExtensions
|
7
|
+
TYPE_CHECKER_CONST = :DIALEKT_TYPE_CHECKER
|
8
|
+
INFLECTOR_CONST = :DIALEKT_INFLECTOR
|
9
|
+
|
10
|
+
module ModuleMixins
|
11
|
+
def dialekt_base_name
|
12
|
+
@__dialekt_base_name ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
|
13
|
+
name.gsub(%r{\A.+::}, "")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def dialekt_enclosing_module
|
18
|
+
if %r{\A(?<parent_name>[^:#]+(?:::[^:#]+)*)::[^:]+\z} =~ name && Kernel.const_defined?(parent_name)
|
19
|
+
Kernel.const_get(parent_name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def dialekt_lookup_type_checker
|
24
|
+
if const_defined?(TYPE_CHECKER_CONST, true)
|
25
|
+
const_get(TYPE_CHECKER_CONST)
|
26
|
+
else
|
27
|
+
enclosing_module = self.dialekt_enclosing_module
|
28
|
+
|
29
|
+
type_checker = if enclosing_module.nil?
|
30
|
+
RubyTypeChecker.instance
|
31
|
+
else
|
32
|
+
enclosing_module.dialekt_type_checker
|
33
|
+
end
|
34
|
+
|
35
|
+
const_set(TYPE_CHECKER_CONST, type_checker)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dialekt_type_checker(checker = EMPTY)
|
40
|
+
if checker == EMPTY
|
41
|
+
dialekt_lookup_type_checker
|
42
|
+
else
|
43
|
+
if const_defined?(TYPE_CHECKER_CONST)
|
44
|
+
raise ArgumentError, "#{self.class} #{self} already has a type checker defined"
|
45
|
+
end
|
46
|
+
|
47
|
+
const_set(TYPE_CHECKER_CONST, checker)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def dialekt_lookup_inflector
|
52
|
+
if const_defined?(INFLECTOR_CONST, true)
|
53
|
+
const_get(INFLECTOR_CONST)
|
54
|
+
else
|
55
|
+
enclosing_module = self.dialekt_enclosing_module
|
56
|
+
|
57
|
+
inflector = if enclosing_module.nil?
|
58
|
+
Dry::Inflector.new
|
59
|
+
else
|
60
|
+
enclosing_module.dialekt_inflector
|
61
|
+
end
|
62
|
+
|
63
|
+
const_set(INFLECTOR_CONST, inflector)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def dialekt_inflector(inflector = EMPTY)
|
68
|
+
if inflector == EMPTY
|
69
|
+
dialekt_lookup_inflector
|
70
|
+
else
|
71
|
+
if const_defined?(INFLECTOR_CONST)
|
72
|
+
raise ArgumentError, "#{self.class} #{self} already has an inflector defined"
|
73
|
+
end
|
74
|
+
|
75
|
+
const_set(INFLECTOR_CONST, inflector)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Module.include(ModuleMixins)
|
81
|
+
|
82
|
+
module CallableExtensions
|
83
|
+
def call_signature
|
84
|
+
CallSignature.create(signature: parameters)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
Proc.include(CallableExtensions)
|
89
|
+
Method.include(CallableExtensions)
|
90
|
+
UnboundMethod.include(CallableExtensions)
|
91
|
+
|
92
|
+
module ProcExtensions
|
93
|
+
def call_adapter
|
94
|
+
CallAdapter.new(callable: self)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Proc.include(ProcExtensions)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|