drymm 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # # Shapes namespace for Dry::Logic
6
+ module Logic
7
+ extend SumEnclosure
8
+
9
+ # @abstract
10
+ # Abstract node branch class for shapes of {Dry::Logic}
11
+ class Rule < Node
12
+ abstract
13
+ extend Branch
14
+ extend Summarize
15
+
16
+ def self.namespace
17
+ Logic
18
+ end
19
+
20
+ # @return [::Dry::Logic::Predicates]
21
+ def self.compiler_registry
22
+ ::Dry::Logic::Predicates
23
+ end
24
+
25
+ # @return [::Dry::Logic::RuleCompiler]
26
+ def self.compiler(registry = compiler_registry())
27
+ ::Dry::Logic::RuleCompiler.new(registry)
28
+ end
29
+ end
30
+
31
+ sum.set Rule
32
+
33
+ # @see Dry::Logic::Predicate::Rule
34
+ class Predicate < Rule
35
+ attribute :type, type_identifier
36
+ attribute :name, Drymm["types.sym"]
37
+ attribute :args, Drymm["types.variadic.any"]
38
+ end
39
+
40
+ # @see Dry::Logic::Operations::Check
41
+ class Check < Rule
42
+ attribute :type, type_identifier
43
+ attribute :keys, Drymm["types.variadic.any"]
44
+ attribute :rule, Drymm["types.sum.rules"]
45
+ end
46
+
47
+ # @see Dry::Logic::Operations::Unary
48
+ class Unary < Rule
49
+ tuple_right false
50
+ attribute :type, type_enum(:each, :not, :negation)
51
+ attribute :rule, Drymm["types.sum.rules"]
52
+
53
+ def self.coerce_tuple((type, rule))
54
+ { type: type, rule: rule }
55
+ end
56
+ end
57
+
58
+ # @see Dry::Logic::Operations::Attr
59
+ # @see Dry::Logic::Operations::Key
60
+ class RoutedUnary < Rule
61
+ attribute :type, type_enum(:attr, :key)
62
+ attribute :path, Drymm["types.any"]
63
+ attribute :rule, Drymm["types.sum.rules"]
64
+ end
65
+
66
+ # @see Dry::Logic::Operations::Binary
67
+ class Grouping < Rule
68
+ tuple_right false
69
+ attribute :type, type_enum(:and, :or, :xor, :set)
70
+ attribute :rules, Drymm["types.variadic.rules"]
71
+
72
+ def to_ast
73
+ [type, rules.map(&:to_ast)]
74
+ end
75
+
76
+ def self.coerce_tuple((type, rules))
77
+ { type: type, rules: rules }
78
+ end
79
+ end
80
+
81
+ # @see Dry::Logic::Operations::Implication
82
+ class BinaryComposition < Rule
83
+ attribute :type, type_enum(:implication)
84
+ attribute :left, Drymm["types.sum.rules"]
85
+ attribute :right, Drymm["types.sum.rules"]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # @abstract
6
+ # {Dry::Struct} abstract subclass, extended with {::Dry::Tuple::StructClassInterface}
7
+ class Node < Dry::Struct
8
+ abstract
9
+
10
+ extend Dry::Tuple::StructClassInterface
11
+ include Dry::Core::Constants
12
+ include JSONMethods
13
+ include ASTMethods
14
+
15
+ class << self
16
+ # @overload type_identifier(klass)
17
+ # Generates identifier from the given class name
18
+ # @param klass [Class]
19
+ # @overload type_identifier(name)
20
+ # Coerces the given name to symbol and uses it as a node type identifier
21
+ # @param name [#to_sym]
22
+ # @overload type_identifier(list)
23
+ # Builds {Dry::Types::Enum} from the given list of values
24
+ # @param list [Array]
25
+ # @return [Dry::Types::Type]
26
+ # coercible symbol type with specific value — node type identifier
27
+ # (declared, for example, as `type` in {Drymm::Shape::LogicNode.inherited})
28
+ # @note
29
+ # To hook the assigned type identifier for specific subclass, just override
30
+ # this method.
31
+ # @example Hook the type value
32
+ # class Something < Drymm::S::Node
33
+ # def self.type_identifier(*)
34
+ # super(:hooked)
35
+ # end
36
+ # end
37
+ def type_identifier(input = self)
38
+ case input
39
+ when ::String, ::Symbol
40
+ Drymm["types.sym"].constrained(eql: input.to_sym)
41
+ when ::Array
42
+ Drymm["types.sym"].enum(*input)
43
+ when ::Module
44
+ type_identifier(Drymm["fn.type_identifier"][input.name])
45
+ else
46
+ raise ArgumentError
47
+ end
48
+ end
49
+
50
+ # Shorthand to declare type as a enum
51
+ # @param input [Array<Symbol>]
52
+ # @return [Dry::Types[Enum<Symbol>]]
53
+ def type_enum(*input)
54
+ type_identifier(input)
55
+ end
56
+
57
+ # @api private
58
+ # Invokes Dry::Tuple::StructClassInterface#auto_tuple each time an
59
+ # attribute is declared.
60
+ def define_accessors(keys)
61
+ super
62
+ auto_tuple(*keys)
63
+ end
64
+ end
65
+
66
+ private_class_method :define_accessors
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # @api private
6
+ module SumEnclosure
7
+ include Dry::Types::Type
8
+ include Dry::Types::Meta
9
+
10
+ # @api private
11
+ def sum
12
+ @sum ||= AtomicType.new
13
+ end
14
+
15
+ # @!method call(input, &block)
16
+ # @overload call(type: :id, id:)
17
+ # @param id [Symbol, String]
18
+ # @overload call(type: :callable, callable:)
19
+ # @param callable [#call]
20
+ # @overload call(type: :method, target:, name:)
21
+ # @param target [#(name)] namespace
22
+ # @param name [Symbol, String]
23
+ # @return [self]
24
+
25
+ # @api private
26
+ def call_unsafe(input)
27
+ @sum.call(input)
28
+ end
29
+
30
+ # @api private
31
+ def call_safe(input, &block)
32
+ @sum.call(input, &block)
33
+ end
34
+
35
+ # @api private
36
+ def try(input, &block)
37
+ @sum.try(input, &block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # @api private
6
+ module Summarize
7
+ private
8
+
9
+ # @api private
10
+ def inherited(subclass)
11
+ super
12
+ namespace.sum.try_update! do |current|
13
+ if current == abstract_class
14
+ subclass
15
+ else
16
+ current | subclass
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # # Shapes namespace for Dry::Types
6
+ module Types
7
+ extend SumEnclosure
8
+
9
+ # @abstract
10
+ # Abstract node branch class for shapes of {Dry::Types}
11
+ class Type < Node
12
+ abstract
13
+ extend Branch
14
+ extend Summarize
15
+
16
+ def self.namespace
17
+ Types
18
+ end
19
+
20
+ # @return [::Dry::Logic::Predicates]
21
+ def self.compiler_registry
22
+ ::Dry::Types
23
+ end
24
+
25
+ # @return [::Dry::Logic::RuleCompiler]
26
+ def self.compiler(registry = compiler_registry())
27
+ ::Dry::Types::Compiler.new(registry)
28
+ end
29
+
30
+ def compile
31
+ self.class.compiler.call(to_ast)
32
+ end
33
+ end
34
+
35
+ sum.set Type
36
+
37
+ # @see Dry::Types::Any
38
+ class Any < Type
39
+ tuple_right false
40
+ attribute :type, type_identifier
41
+ attribute :meta, Drymm["types.meta"]
42
+ end
43
+
44
+ # @see Dry::Types::Array
45
+ class Array < Type
46
+ attribute :type, type_identifier
47
+ attribute :member, Drymm["types.sum.types"]
48
+ attribute :meta, Drymm["types.meta"]
49
+ end
50
+
51
+ # @see Dry::Types::JSON
52
+ # @see Dry::Types::Params
53
+ class CoercibleArray < Type
54
+ attribute :type, type_enum(:json_array, :params_array)
55
+ attribute :member, Drymm["types.sum.types"]
56
+ end
57
+
58
+ # @see Dry::Types::JSON
59
+ # @see Dry::Types::Params
60
+ class CoercibleHash < Type
61
+ attribute :type, type_enum(:json_hash, :params_hash)
62
+ attribute :keys, Drymm["types.variadic.types"]
63
+ attribute :meta, Drymm["types.meta"]
64
+ end
65
+
66
+ # @see Dry::Types::Composition
67
+ class Composition < Type
68
+ attribute :type, type_enum(:implication, :intersection, :transition, :sum)
69
+ attribute :left, Drymm["types.sum.types"]
70
+ attribute :right, Drymm["types.sum.types"]
71
+ attribute :meta, Drymm["types.meta"]
72
+ end
73
+
74
+ # @see Dry::Types::Constrained
75
+ class Constrained < Type
76
+ attribute :type, type_identifier
77
+ attribute :base, Drymm["types.sum.types"]
78
+ attribute :rule, Drymm["types.sum.rules"]
79
+ end
80
+
81
+ # @see Dry::Types::Constructor
82
+ class Constructor < Type
83
+ attribute :type, type_identifier
84
+ attribute :base, Drymm["types.sum.types"]
85
+ attribute :fn, Drymm["types.fn"]
86
+ end
87
+
88
+ # @see Dry::Types::Enum
89
+ class Enum < Type
90
+ attribute :type, type_identifier
91
+ attribute :base, Drymm["types.sum.types"]
92
+ attribute :mapping, Drymm["types.hash"]
93
+ end
94
+
95
+ # @see Dry::Types::Hash
96
+ class Hash < Type
97
+ attribute :type, type_identifier
98
+ attribute :options, Drymm["types.opts"]
99
+ attribute :meta, Drymm["types.meta"]
100
+ end
101
+
102
+ # @see Dry::Types::Schema::Key
103
+ class Key < Type
104
+ attribute :type, type_identifier
105
+ attribute :name, Drymm["types.any"]
106
+ attribute :required, Drymm["types.bool"]
107
+ attribute :base, Drymm["types.sum.types"]
108
+ end
109
+
110
+ # @see Dry::Types::Lax
111
+ class Lax < Type
112
+ tuple_right false
113
+ attribute :type, type_identifier
114
+ attribute :base, Drymm["types.sum.types"]
115
+
116
+ def self.coerce_tuple((type, base))
117
+ { type: type, base: base }
118
+ end
119
+ end
120
+
121
+ # @see Dry::Types::Map
122
+ class Map < Type
123
+ attribute :type, type_identifier
124
+ attribute :key_type, Drymm["types.sum.types"]
125
+ attribute :value_type, Drymm["types.sum.types"]
126
+ attribute :meta, Drymm["types.meta"]
127
+ end
128
+
129
+ # @see Dry::Types::Nominal
130
+ class Nominal < Type
131
+ attribute :type, type_identifier
132
+ attribute :base, Drymm["types.const"]
133
+ attribute :meta, Drymm["types.meta"]
134
+ end
135
+
136
+ # @see Dry::Types::Schema
137
+ class Schema < Type
138
+ attribute :type, type_identifier
139
+ attribute :keys, Drymm["types.variadic.types"]
140
+ attribute :options, Drymm["types.opts"]
141
+ attribute :meta, Drymm["types.meta"]
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ # The core Shapes namespace
5
+ module Shapes
6
+ include Dry::Core::Constants
7
+
8
+ # @api private
9
+ module Mix
10
+ private
11
+
12
+ def included(base)
13
+ base.extend(const_get(:ClassMethods))
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def sum
19
+ Drymm["sum"]
20
+ end
21
+
22
+ def call(input)
23
+ sum[input]
24
+ end
25
+
26
+ alias [] call
27
+ end
28
+
29
+ extend JSONMethods::ClassMethods
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ VERSION = "0.2.0"
5
+ end
data/lib/drymm.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "drymm/version"
4
+ require "zeitwerk"
5
+ require "dry/core/constants"
6
+ require "dry/logic"
7
+ require "dry/logic/predicates"
8
+ require "dry/logic/builder"
9
+ require "dry/types"
10
+ require "dry/struct"
11
+ require "dry/monads/all"
12
+ require "dry/tuple"
13
+
14
+ # Load patches for dry-rb gems if need
15
+
16
+ unless Dry::Types::Printer.method_defined?(:visit_sum_constructors)
17
+ require_relative "dry/types/printer/visit_sum_constructors"
18
+ end
19
+
20
+ unless Dry::Logic::Builder::Context.instance_method(:predicate).parameters.include?(%i[opt context])
21
+ require_relative "dry/logic/builder/context_predicate_name"
22
+ end
23
+
24
+ require_relative "drymm/inflector"
25
+
26
+ # # Drym¹m² is for (¹)meta (²)mapping
27
+ #
28
+ # Drymm represents entities provided by {Dry::Logic} {Dry::Types}
29
+ # as a {Dry::Struct} classes under a {Drymm::Shapes} namespace.
30
+ #
31
+ # The core feature of Drymm::Shapes is an ability to cast an AST produced
32
+ # by that entities and structurize it for the following serialization (for example,
33
+ # into JSON). Also it provides an interface to load serialized data and compile it back
34
+ # to the Type or Logic entity.
35
+ #
36
+ # The casts perform by declaring expecting shapes under a specific {Drymm::Shapes::Branch}
37
+ # namespace without any conditional code but with a significant amount of recursion.
38
+ # Declared shapes composed into {Dry::Struct::Sum} which are in front of the casting behaviour.
39
+ module Drymm
40
+ class << self
41
+ # @!visibility private
42
+
43
+ def loader
44
+ @loader ||= Zeitwerk::Loader.for_gem.tap do |loader|
45
+ if $PROGRAM_NAME == "bin/console"
46
+ loader.enable_reloading
47
+ loader.log!
48
+ end
49
+ loader.tag = "drymm"
50
+ loader.inflector = Drymm::Inflector.new(__FILE__)
51
+ loader.ignore "#{__dir__}/dry"
52
+ loader.collapse "#{__dir__}/drymm/repos"
53
+ end
54
+ end
55
+
56
+ # @return [Drymm::Inflector]
57
+ def inflector
58
+ loader.inflector
59
+ end
60
+ end
61
+
62
+ loader.setup
63
+
64
+ include Dry::Core::Constants
65
+
66
+ extend Container
67
+
68
+ # @!method self.[](key)
69
+ # @param [String, Symbol] key
70
+ # @return [mixed]
71
+
72
+ # @!method self.resolve(key)
73
+ # @param [String, Symbol] key
74
+ # @return [mixed]
75
+
76
+ merge Drymm::RulesRepo, namespace: :rules
77
+ merge Drymm::FnRepo, namespace: :fn
78
+ merge Drymm::TypesRepo, namespace: :types
79
+
80
+ logic_builder = self["fn.logic_builder"]
81
+
82
+ keys.grep(/^rules/).each do |key|
83
+ decorate key, with: logic_builder
84
+ end
85
+
86
+ register :sum, memoize: true do
87
+ (
88
+ resolve("types.sum.rules") |
89
+ resolve("types.sum.types")
90
+ ).meta(recursive: "poly")
91
+ end
92
+ end