dialekt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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