drymm 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d8f12bd0894d16b195930e4bea3fa4e752483cb2c92f942b90318999951f0ff9
4
+ data.tar.gz: c50dc6947861850fcaa539690209b559dd74458f9d3a3dfe37e8e43ba4a39fea
5
+ SHA512:
6
+ metadata.gz: 66f843c85fc7bed6d84c1fc333f3b7c9a1168a806ba589c8cf985d99b543522d9fb568ded67efa28375e5e1a393f263542db7f05a8a54f0fe9e744047d69cbc0
7
+ data.tar.gz: 573fa9341ef612207a5c97dbd06d5f8c116b9c438865b92f0d50ff05ac8f63f129566f12bd128dbf1490bc376fd221f185f240f8ece13bae9ab215d734008c8a
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Drym¹m² is for (¹)meta (²)mapping
2
+
3
+ Drymm represents entities provided by `Dry::Logic` & `Dry::Types` as a `Dry::Struct` classes under a `Drymm::Shapes` namespace.
4
+
5
+ The core feature of `Drymm::Shapes` is an ability to cast an AST produced by that entities and structurize it for the following serialization. Also it provides an interface to load serialized data and compile it back the Type or Logic entity.
6
+
7
+ The casts perform by declaring expecting shapes under a specific `Drymm::Shapes::Branch` without any conditional code but with a significant amount of recursion. Shapes composed into `Dry::Struct::Sum` and handled by `Concurrent::AtomicReference` which are in front of the casting behaviour.
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add drymm
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install drymm
18
+
19
+ ## Usage
20
+
21
+
22
+ ## Development
23
+
24
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
25
+
26
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
27
+
28
+ ## Contributing
29
+
30
+ Bug reports and pull requests are welcome on GitHub at https://github.com/estum/drymm. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/estum/drymm/blob/main/CODE_OF_CONDUCT.md).
31
+
32
+ ## License
33
+
34
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
35
+
36
+ ## Code of Conduct
37
+
38
+ Everyone interacting in the Drymm project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/estum/drymm/blob/main/CODE_OF_CONDUCT.md).
data/drymm.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/drymm/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "drymm"
7
+ spec.authors = ["estum"]
8
+ spec.email = ["anton.estum@gmail.com"]
9
+ spec.license = "MIT"
10
+ spec.version = Drymm::VERSION
11
+
12
+ spec.summary = "Universal meta mapper for dry-logic & dry-types."
13
+ spec.description = "Drymm maps entities from Dry::Logic & Dry::Types into structs for a serialization purpose."
14
+ spec.homepage = "https://github.com/estum/drymm"
15
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "drymm.gemspec", "lib/**/*"]
16
+ spec.bindir = "bin"
17
+ spec.executables = []
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/estum/drymm"
23
+ spec.metadata["changelog_uri"] = "https://github.com/estum/drymm/blob/main/CHANGELOG.md"
24
+
25
+ spec.required_ruby_version = ">= 2.6.0"
26
+
27
+ spec.add_runtime_dependency "concurrent-ruby"
28
+ spec.add_runtime_dependency "concurrent-ruby-ext"
29
+ spec.add_runtime_dependency "dry-core"
30
+ spec.add_runtime_dependency "dry-inflector"
31
+ spec.add_runtime_dependency "dry-logic"
32
+ spec.add_runtime_dependency "dry-monads"
33
+ spec.add_runtime_dependency "dry-struct"
34
+ spec.add_runtime_dependency "dry-types"
35
+ spec.add_runtime_dependency "dry-types-tuple", ">= 0.1.4"
36
+ spec.add_runtime_dependency "zeitwerk"
37
+
38
+ spec.add_development_dependency "bundler"
39
+ spec.add_development_dependency "rake"
40
+ spec.add_development_dependency "rspec"
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Logic
5
+ module Builder
6
+ # @api private
7
+ class Context
8
+ # Defines custom predicate
9
+ #
10
+ # @name [Symbol] Name of predicate
11
+ # @Context [Proc]
12
+ def predicate(name, context = nil, &block)
13
+ singleton_class.undef_method(name) if singleton_class.method_defined?(name)
14
+
15
+ predicate = Rule::Predicate.new(context || block)
16
+
17
+ define_singleton_method(name) do |*args|
18
+ predicate.curry(*args)
19
+ end
20
+ end
21
+
22
+ # Defines methods for operations and predicates
23
+ def initialize
24
+ Operations.constants(false).each do |name|
25
+ next if Dry::Logic::Builder::IGNORED_OPERATIONS.include?(name)
26
+
27
+ operation = Operations.const_get(name)
28
+
29
+ define_singleton_method(name.downcase) do |*args, **kwargs, &block|
30
+ operation.new(*call(&block), *args, **kwargs)
31
+ end
32
+ end
33
+
34
+ Predicates::Methods.instance_methods(false).each do |name|
35
+ predicate(name, Predicates[name]) unless Dry::Logic::Builder::IGNORED_PREDICATES.include?(name)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # @api private
6
+ class Printer
7
+ alias visit_sum_constructors visit_composition
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/version"
4
+
5
+ module Drymm
6
+ # @api private
7
+ module Container
8
+ if Dry::Core::VERSION >= "1.0.0"
9
+ include Dry::Core::Container::Mixin
10
+ else
11
+ require "dry/container"
12
+ include Dry::Container::Mixin
13
+ end
14
+
15
+ # @private
16
+ def self.extended(base)
17
+ Dry::Core::Container::Mixin.extended(base)
18
+ end
19
+
20
+ private
21
+
22
+ def alias_item(new_name, old_name)
23
+ register new_name, memoize: true do
24
+ resolve(old_name)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ module Drymm
6
+ # @api private
7
+ class Inflector < Dry::Inflector
8
+ def initialize(root_file)
9
+ super() do |inflections|
10
+ inflections.acronym "AST"
11
+ yield(inflections) if block_given?
12
+ end
13
+ namespace = File.basename(root_file, ".rb")
14
+ lib_dir = File.dirname(root_file)
15
+ @version_file = File.join(lib_dir, namespace, "version.rb")
16
+ end
17
+
18
+ def camelize(basename, abspath)
19
+ abspath == @version_file ? "VERSION" : super(basename)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ # @api private
5
+ module FnRepo
6
+ extend Container
7
+
8
+ register :ary_wrap do |input|
9
+ Drymm["rules.ary"][input] ? input : [input]
10
+ end
11
+
12
+ register :logic_builder do |fn|
13
+ Dry::Logic::Builder.call(&fn)
14
+ end
15
+
16
+ register :type_identifier do |name|
17
+ Drymm.inflector.underscore(Drymm.inflector.demodulize(name)).to_sym
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ # @api private
5
+ module RulesRepo
6
+ extend Container
7
+
8
+ register :const,
9
+ proc { key(name: 0) { case?(Drymm["types.const"]) } },
10
+ call: false
11
+
12
+ register :ary,
13
+ proc { array? },
14
+ call: false
15
+
16
+ register :node,
17
+ proc { array? & min_size?(1) & key(name: 0) { negation { array? } } },
18
+ call: false
19
+
20
+ register :as_ast,
21
+ proc { respond_to?(:to_ast) },
22
+ call: false
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ # @api private
5
+ module TypesRepo
6
+ extend Container
7
+
8
+ register :any, Dry::Types["any"]
9
+
10
+ register :ary, Dry::Types["array"]
11
+
12
+ register :bool, Dry::Types["bool"]
13
+
14
+ register :hash, Dry::Types["hash"]
15
+
16
+ register :opts, (Dry::Types["hash"].default { Drymm::EMPTY_OPTS })
17
+ alias_item :meta, :opts
18
+
19
+ register :sym, Dry::Types["coercible.symbol"]
20
+
21
+ register :str, Dry::Types["coercible.string"]
22
+
23
+ T = method def self.t(member)
24
+ return member if !member.is_a?(String) && !member.is_a?(Symbol)
25
+
26
+ resolve(member) { member }
27
+ end
28
+
29
+ Array = method def self.array(member)
30
+ t(:ary_wrap).of t(member)
31
+ end
32
+
33
+ Tuple = method def self.tuple(*members)
34
+ Dry::Types::Tuple.build_unsplat(members)
35
+ end
36
+
37
+ Constrained = method def self.constrained(member, rule)
38
+ Dry::Types::Constrained.new(t(member), rule: rule)
39
+ end
40
+
41
+ register "sum.types", proc { Shapes::Types.sum }, memoize: false
42
+
43
+ register "sum.rules", proc { Shapes::Logic.sum }, memoize: false
44
+
45
+ register "variadic.any", proc { Array[:any] }, memoize: true
46
+
47
+ register "variadic.types", proc { Array["sum.types"] }, memoize: false
48
+
49
+ register "variadic.rules", proc { Array["sum.rules"] }, memoize: false
50
+
51
+ register :fn, proc { Shapes::Fn.sum }, memoize: false
52
+
53
+ register :const, proc { Shapes::Const }, memoize: false
54
+ alias_item :class, :const
55
+
56
+ register :ast, proc { constrained(:any, Drymm["rules.as_ast"]) }, memoize: true
57
+
58
+ register :ary_wrap, proc { T[:ary] << Drymm["fn.ary_wrap"] }, memoize: true
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # AST-related methods mixin
6
+ module ASTMethods
7
+ extend Mix
8
+
9
+ # Fold data back into the plain AST
10
+ # @return [Array]
11
+ def to_ast
12
+ type, *node = attributes.values_at(*self.class.keys_order)
13
+ node = recursive_ast(node)
14
+ node = node[0] if node.size == 1
15
+ [type, node]
16
+ end
17
+
18
+ # Compile an instance back to original object
19
+ # @see ClassMethods#compiler
20
+ # @see ClassMethods#compiler_registry
21
+ # @return [Object]
22
+ def compile
23
+ self.class.compiler.call([to_ast])[0]
24
+ end
25
+
26
+ private
27
+
28
+ def recursive_ast(node)
29
+ case node
30
+ when Array
31
+ node.map { |item| recursive_ast(item) }
32
+ when Hash
33
+ node.transform_values { |item| recursive_ast(item) }
34
+ when Drymm["types.ast"]
35
+ node.to_ast
36
+ else
37
+ node
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ module ClassMethods
43
+ # @abstract
44
+ # Should be overriden in subclasses to return specific compiler
45
+ # @param registry [container]
46
+ # @return [#call]
47
+ def compiler(registry = compiler_registry())
48
+ raise NotImplementedError
49
+ end
50
+
51
+ # @abstract
52
+ # Should be overriden in subclasses to return specific registry
53
+ # @return [container]
54
+ def compiler_registry
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # @api private
6
+ class AtomicType < Concurrent::AtomicReference
7
+ include Dry::Types::Type
8
+ include Dry::Types::Builder
9
+ include Dry::Types::Decorator
10
+ include Dry::Equalizer(:type, inspect: false, immutable: false)
11
+
12
+ def initialize(initial_type = Drymm::Undefined)
13
+ Drymm::Undefined.map(initial_type) do
14
+ set(initial_type)
15
+ end
16
+ end
17
+
18
+ alias type get
19
+
20
+ def to_s
21
+ Dry::Types::PRINTER.(get) { get.to_s }
22
+ end
23
+
24
+ alias inspect to_s
25
+
26
+ def respond_to_missing?(method_name, include_private = true)
27
+ get.respond_to?(method_name, include_private) || super
28
+ end
29
+
30
+ def method_missing(method_name, *args, **opts, &blk)
31
+ value = get
32
+
33
+ if value.respond_to?(method_name, true)
34
+ value.send(method_name, *args, **opts, &blk)
35
+ else
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # {Drymm::Shapes}'s class interface mixin, designed to extend
6
+ # an abstract __subclass__ of {Drymm::Shapes::Node}, creating a kind of
7
+ # Shape's branch or, in other words, derivative, to enclose specific namespace.
8
+ module Branch
9
+ # @api private
10
+ # Defines {sum} class attribute when extended.
11
+ private_class_method def self.extended(base)
12
+ base.defines :tuple_right
13
+ base.tuple_right true
14
+ base.private_class_method :auto_tuple
15
+ end
16
+
17
+ # @api private
18
+ # Flattens the 1st level of the input array.
19
+ def coerce_tuple(input)
20
+ super(input.flatten(1))
21
+ end
22
+
23
+ # @api private
24
+ def auto_tuple(*keys)
25
+ keys_order(keys_order | keys)
26
+ index = schema.keys.map { |t| [t.name, t.type] }.to_h
27
+ key_type, *node_types = index.values_at(*keys_order)
28
+ if tuple_right
29
+ tuple Tuple(key_type, Tuple(*node_types))
30
+ else
31
+ tuple Tuple(key_type, *node_types)
32
+ end
33
+ end
34
+
35
+ def retuple
36
+ auto_tuple!(*keys_order)
37
+ end
38
+
39
+ def auto_tuple!(*keys)
40
+ remove_instance_variable(:@keys_order) if instance_variable_defined?(:@keys_order)
41
+ remove_instance_variable(:@tuple) if instance_variable_defined?(:@tuple)
42
+ keys_order []
43
+ auto_tuple(*keys)
44
+ end
45
+
46
+ private
47
+
48
+ # Shorthand method to build a tuple
49
+ # @return [Dry::Types::Tuple]
50
+ def Tuple(*args)
51
+ Drymm::TypesRepo.tuple(*args)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # The intermediary shape class to handle bidirectional
6
+ # serialization of classes & modules, referenced in the node's AST.
7
+ #
8
+ # @example Wraps
9
+ # Drymm::Shapes::Const[Dry]
10
+ # # => #<Drymm::Shapes::Const name='Dry'>
11
+ #
12
+ # Drymm::Shapes::Const[name: 'Dry'].to_literal
13
+ # # => Dry
14
+ class Const < Node
15
+ attribute :type, (type_identifier(:const).default { :const })
16
+ attribute :name, Drymm["types.str"]
17
+
18
+ include Dry.Equalizer(:name, immutable: true)
19
+
20
+ # Constantize by name
21
+ # @return [Class, Module]
22
+ def to_literal
23
+ ::Object.const_get(name)
24
+ end
25
+
26
+ alias to_ast to_literal
27
+
28
+ class << self
29
+ # @!method call(input, &block)
30
+ # @overload call(class_or_module_literal)
31
+ # @param class_or_module_literal [Class, Module]
32
+ # @overload call(hash)
33
+ # @param hash [Hash { :type => :const, :name => String }]
34
+ # @return [self]
35
+
36
+ # @return [Class(Module)]
37
+ def primitive
38
+ ::Module
39
+ end
40
+
41
+ # @param input [Any]
42
+ # @return [true] if the input is an instance of {Module} class.
43
+ def primitive?(input)
44
+ input.is_a?(primitive)
45
+ end
46
+
47
+ # @api private
48
+ def call_unsafe(input)
49
+ return super unless primitive?(input)
50
+
51
+ super(name: input)
52
+ end
53
+
54
+ # @api private
55
+ def call_safe(input, &block)
56
+ return super unless primitive?(input)
57
+
58
+ super(name: input, &block)
59
+ end
60
+
61
+ # @api private
62
+ def try(input, &block)
63
+ return super unless primitive?(input)
64
+
65
+ super(name: input, &block)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Drymm
4
+ module Shapes
5
+ # Namespace of function shapes, i.e. procs or methods, used by {Dry::Types}
6
+ module Fn
7
+ extend SumEnclosure
8
+
9
+ # @abstract
10
+ # Abstract node branch class for shapes of {Dry::Types::Constructor::Function}
11
+ class FnNode < Node
12
+ abstract
13
+ extend Summarize
14
+
15
+ attribute :type, type_enum(:id, :callable, :method)
16
+
17
+ def self.namespace
18
+ Fn
19
+ end
20
+
21
+ # @return [::Dry::Logic::Predicates]
22
+ def self.compiler_registry
23
+ ::Dry::Types.container
24
+ end
25
+
26
+ # @return [::Dry::Logic::RuleCompiler]
27
+ def self.compiler(registry = compiler_registry())
28
+ ::Dry::Types::Compiler.new(registry)
29
+ end
30
+
31
+ def compile
32
+ self.class.compiler.compile_fn(to_ast)
33
+ end
34
+ end
35
+
36
+ sum.set FnNode
37
+
38
+ # Represents functions, stored in {Dry::Types::FnContainer}
39
+ class ID < FnNode
40
+ attribute :type, type_identifier(:id)
41
+ attribute :id, Drymm["types.str"]
42
+ end
43
+
44
+ # Represent callable objects but not an internal procs
45
+ class Callable < FnNode
46
+ attribute :type, type_identifier(:callable)
47
+ attribute :callable, Drymm["types.any"]
48
+
49
+ # @!attribute callable [r]
50
+ # @return [#call]
51
+ end
52
+
53
+ # Represents a method calls
54
+ class Method < FnNode
55
+ attribute :type, type_identifier(:method)
56
+ attribute :target, Drymm["types.const"] | Drymm["types.any"]
57
+ attribute :name, Drymm["types.sym"]
58
+
59
+ def to_ast
60
+ super.flatten(1)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Drymm
6
+ module Shapes
7
+ # JSON serialization methods mixin.
8
+ module JSONMethods
9
+ extend Mix
10
+
11
+ # Dumps the instance to a JSON string.
12
+ # @param pretty [Hash]
13
+ # @return [String]
14
+ def to_json(pretty: nil)
15
+ if pretty
16
+ JSON.pretty_generate(to_hash, pretty)
17
+ else
18
+ JSON.fast_generate(to_hash)
19
+ end
20
+ end
21
+
22
+ # @api private
23
+ module ClassMethods
24
+ # Parse JSON to a shape node
25
+ def from_json(payload)
26
+ call(JSON.parse(payload, symbolize_names: true))
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end