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.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +31 -0
- data/.ruby-version +1 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +305 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/hashcraft.gemspec +38 -0
- data/lib/hashcraft.rb +22 -0
- data/lib/hashcraft/base.rb +70 -0
- data/lib/hashcraft/compiler.rb +80 -0
- data/lib/hashcraft/core_ext/hash.rb +21 -0
- data/lib/hashcraft/dsl.rb +49 -0
- data/lib/hashcraft/generic.rb +11 -0
- data/lib/hashcraft/generic/dictionary.rb +54 -0
- data/lib/hashcraft/generic/registry.rb +65 -0
- data/lib/hashcraft/mutator_registry.rb +26 -0
- data/lib/hashcraft/mutators/array.rb +25 -0
- data/lib/hashcraft/mutators/hash.rb +25 -0
- data/lib/hashcraft/mutators/property.rb +24 -0
- data/lib/hashcraft/option.rb +62 -0
- data/lib/hashcraft/transformer_registry.rb +26 -0
- data/lib/hashcraft/transformers/camel_case.rb +27 -0
- data/lib/hashcraft/transformers/pascal_case.rb +25 -0
- data/lib/hashcraft/transformers/pass_thru.rb +21 -0
- data/lib/hashcraft/version.rb +12 -0
- metadata +179 -0
data/Rakefile
ADDED
@@ -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]
|
data/bin/console
ADDED
@@ -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
|
data/hashcraft.gemspec
ADDED
@@ -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
|
data/lib/hashcraft.rb
ADDED
@@ -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
|