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