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
@@ -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
|