hashcraft 1.0.0.pre.alpha.1

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,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