hashcraft 1.0.0.pre.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|