hashcraft 1.0.0.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'bundler/gem_tasks'
11
+ require 'rspec/core/rake_task'
12
+ require 'rubocop/rake_task'
13
+
14
+ RSpec::Core::RakeTask.new(:spec)
15
+ RuboCop::RakeTask.new
16
+
17
+ task default: %i[rubocop spec]
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
6
+ #
7
+ # This source code is licensed under the MIT license found in the
8
+ # LICENSE file in the root directory of this source tree.
9
+ #
10
+
11
+ require 'bundler/setup'
12
+ require 'hashcraft'
13
+ require 'pry'
14
+
15
+ Pry.start
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/hashcraft/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'hashcraft'
7
+ s.version = Hashcraft::VERSION
8
+ s.summary = 'Hash-based Data Contracting Domain Specific Language'
9
+
10
+ s.description = <<-DESCRIPTION
11
+ Provides a DSL for implementing classes which can then be consumed to create pre-defined hashes.
12
+ DESCRIPTION
13
+
14
+ s.authors = ['Matthew Ruggio']
15
+ s.email = ['mruggio@bluemarblepayroll.com']
16
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ s.bindir = 'exe'
18
+ s.executables = []
19
+ s.homepage = 'https://github.com/bluemarblepayroll/hashcraft'
20
+ s.license = 'MIT'
21
+ s.metadata = {
22
+ 'bug_tracker_uri' => 'https://github.com/bluemarblepayroll/hashcraft/issues',
23
+ 'changelog_uri' => 'https://github.com/bluemarblepayroll/hashcraft/blob/master/CHANGELOG.md',
24
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/hashcraft',
25
+ 'homepage_uri' => s.homepage,
26
+ 'source_code_uri' => s.homepage
27
+ }
28
+
29
+ s.required_ruby_version = '>= 2.5'
30
+
31
+ s.add_development_dependency('guard-rspec', '~>4.7')
32
+ s.add_development_dependency('pry', '~>0')
33
+ s.add_development_dependency('rake', '~> 13')
34
+ s.add_development_dependency('rspec')
35
+ s.add_development_dependency('rubocop', '~>0.79.0')
36
+ s.add_development_dependency('simplecov', '~>0.17.0')
37
+ s.add_development_dependency('simplecov-console', '~>0.6.0')
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'forwardable'
11
+ require 'singleton'
12
+
13
+ # Monkey-patching core libraries
14
+ require_relative 'hashcraft/core_ext/hash'
15
+ Hash.include Hashcraft::CoreExt::Hash
16
+
17
+ # General tooling
18
+ require_relative 'hashcraft/generic'
19
+
20
+ require_relative 'hashcraft/base'
21
+ require_relative 'hashcraft/mutator_registry'
22
+ require_relative 'hashcraft/transformer_registry'
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'compiler'
11
+ require_relative 'dsl'
12
+
13
+ module Hashcraft
14
+ # Super-class for craftable objects.
15
+ class Base
16
+ extend Dsl
17
+ extend Forwardable
18
+
19
+ def_delegators :'self.class',
20
+ :option?,
21
+ :option_set,
22
+ :find_option
23
+
24
+ def initialize(opts = {}, &block)
25
+ @data = make_default_data
26
+
27
+ load_opts(opts)
28
+
29
+ return unless block_given?
30
+
31
+ if block.arity == 1
32
+ yield self
33
+ else
34
+ instance_eval(&block)
35
+ end
36
+ end
37
+
38
+ def to_h(key_transformer: nil, value_transformer: nil)
39
+ Compiler.new(
40
+ option_set,
41
+ key_transformer: key_transformer,
42
+ value_transformer: value_transformer
43
+ ).evaluate!(data)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :data
49
+
50
+ def make_default_data
51
+ option_set.values.each_with_object({}) { |o, memo| o.default!(memo) }
52
+ end
53
+
54
+ def load_opts(opts)
55
+ (opts || {}).each { |k, v| send(k, v) }
56
+ end
57
+
58
+ def respond_to_missing?(method_name, include_private = false)
59
+ option?(method_name) || super
60
+ end
61
+
62
+ def method_missing(method_name, *arguments, &block)
63
+ if option?(method_name)
64
+ find_option(method_name).value!(data, arguments.first, &block)
65
+ else
66
+ super
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Hashcraft
11
+ # This class understands how to traverse an option chain and output a hash.
12
+ class Compiler
13
+ attr_reader :key_transformer, :option_set, :value_transformer
14
+
15
+ def initialize(option_set, key_transformer: nil, value_transformer: nil)
16
+ raise ArgumentError, 'option_set is required' unless option_set
17
+
18
+ @option_set = option_set
19
+ @key_transformer = TransformerRegistry.resolve(key_transformer)
20
+ @value_transformer = TransformerRegistry.resolve(value_transformer)
21
+
22
+ freeze
23
+ end
24
+
25
+ def evaluate!(data)
26
+ data.each_with_object({}) do |(key, value), memo|
27
+ option = option_set.find(key)
28
+
29
+ evaluate_single!(memo, option, value)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :data
36
+
37
+ def evaluate_single!(data, option, value)
38
+ key = option.key.empty? ? option.name : option.key
39
+ transformed_key = key_transformer.transform(key, option)
40
+
41
+ method = value.is_a?(Array) ? :evaluate_values! : :evaluate_value!
42
+
43
+ send(method, option, data, transformed_key, value)
44
+
45
+ self
46
+ end
47
+
48
+ def evaluate_values!(option, data, key, values)
49
+ data[key] ||= []
50
+
51
+ values.each do |value|
52
+ data[key] <<
53
+ if value.is_a?(Hashcraft::Base)
54
+ value.to_h(
55
+ key_transformer: key_transformer,
56
+ value_transformer: value_transformer
57
+ )
58
+ else
59
+ value_transformer.transform(value, option)
60
+ end
61
+ end
62
+
63
+ self
64
+ end
65
+
66
+ def evaluate_value!(option, data, key, value)
67
+ data[key] =
68
+ if value.is_a?(Hashcraft::Base)
69
+ value.to_h(
70
+ key_transformer: key_transformer,
71
+ value_transformer: value_transformer
72
+ )
73
+ else
74
+ value_transformer.transform(value, option)
75
+ end
76
+
77
+ self
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Hashcraft
11
+ module CoreExt
12
+ # Monkey-patches for the core Hash class. These will be manually mixed in separately.
13
+ module Hash
14
+ unless method_defined?(:symbolize_keys)
15
+ def symbolize_keys
16
+ map { |k, v| [k.to_sym, v] }.to_h
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'option'
11
+
12
+ module Hashcraft
13
+ # The class API used to define options for a craftable class. Each class stores its own
14
+ # OptionSet instance along with materializing one for its
15
+ # inheritance chain (child has precedence.)
16
+ module Dsl
17
+ def option?(name)
18
+ option_set.exist?(name)
19
+ end
20
+
21
+ def find_option(name)
22
+ option_set.find(name)
23
+ end
24
+
25
+ def option(*args)
26
+ opts = args.last.is_a?(Hash) ? args.pop : {}
27
+
28
+ args.each do |key|
29
+ option = Option.new(key, opts)
30
+
31
+ local_option_set.add(option)
32
+ end
33
+
34
+ self
35
+ end
36
+
37
+ def option_set
38
+ @option_set ||=
39
+ ancestors
40
+ .reverse
41
+ .select { |a| a < Base }
42
+ .each_with_object(Generic::Dictionary.new) { |a, memo| memo.merge!(a.local_option_set) }
43
+ end
44
+
45
+ def local_option_set
46
+ @local_option_set ||= Generic::Dictionary.new(key: :name)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'generic/dictionary'
11
+ require_relative 'generic/registry'
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Hashcraft
11
+ module Generic
12
+ # Dictionary structure defining how we want to organize objects. Basically a type-insensitive
13
+ # hash where each key is the object's value for the specified key.
14
+ # All keys are #to_s evaluated in order to achieve the type-insensitivity.
15
+ class Dictionary
16
+ extend Forwardable
17
+
18
+ attr_reader :key, :map
19
+
20
+ def_delegators :map, :values
21
+
22
+ def initialize(key: :key)
23
+ raise ArgumentError, 'key is required' if key.to_s.empty?
24
+
25
+ @key = key
26
+ @map = {}
27
+
28
+ freeze
29
+ end
30
+
31
+ def exist?(key)
32
+ !find(key).nil?
33
+ end
34
+
35
+ def find(key)
36
+ @map[key.to_s]
37
+ end
38
+
39
+ def add(object)
40
+ object_key = object.send(key)
41
+
42
+ @map[object_key] = object
43
+
44
+ self
45
+ end
46
+
47
+ def merge!(other)
48
+ map.merge!(other.map)
49
+
50
+ self
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Hashcraft
11
+ module Generic
12
+ # A general data structure that can register and resolve objects by name.
13
+ # It also will act as a pass-thru if a non-string or non-symbol is passed through.
14
+ class Registry
15
+ include Singleton
16
+
17
+ class << self
18
+ extend Forwardable
19
+
20
+ def_delegators :instance,
21
+ :register,
22
+ :register_all,
23
+ :resolve
24
+ end
25
+
26
+ def initialize(map = {})
27
+ @map = {}
28
+
29
+ register_all(map)
30
+
31
+ freeze
32
+ end
33
+
34
+ def register_all(map)
35
+ (map || {}).each { |k, v| register(k, v) }
36
+
37
+ self
38
+ end
39
+
40
+ def register(name, value)
41
+ @map[name.to_s] = value
42
+
43
+ self
44
+ end
45
+
46
+ def resolve(value)
47
+ return value unless lookup?(value)
48
+
49
+ mutator = map[value.to_s]
50
+
51
+ raise ArgumentError, "registration: #{value} not found" unless mutator
52
+
53
+ mutator
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :map
59
+
60
+ def lookup?(value)
61
+ value.is_a?(String) || value.is_a?(Symbol) || value.nil?
62
+ end
63
+ end
64
+ end
65
+ end