hashematics 1.0.0
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 +4 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +123 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +660 -0
- data/bin/benchmark +134 -0
- data/bin/console +11 -0
- data/hashematics.gemspec +32 -0
- data/lib/hashematics.rb +10 -0
- data/lib/hashematics/category.rb +67 -0
- data/lib/hashematics/configuration.rb +90 -0
- data/lib/hashematics/dictionary.rb +72 -0
- data/lib/hashematics/graph.rb +62 -0
- data/lib/hashematics/group.rb +57 -0
- data/lib/hashematics/hashematics.rb +36 -0
- data/lib/hashematics/id.rb +29 -0
- data/lib/hashematics/key.rb +76 -0
- data/lib/hashematics/object_interface.rb +35 -0
- data/lib/hashematics/record.rb +60 -0
- data/lib/hashematics/record_set.rb +29 -0
- data/lib/hashematics/type.rb +72 -0
- data/lib/hashematics/version.rb +12 -0
- data/lib/hashematics/visitor.rb +50 -0
- data/spec/examples/person.rb +36 -0
- data/spec/fixtures/config.yml +44 -0
- data/spec/fixtures/data.csv +9 -0
- data/spec/fixtures/people.yml +84 -0
- data/spec/hashematics/category_spec.rb +62 -0
- data/spec/hashematics/graph_spec.rb +572 -0
- data/spec/hashematics/key_spec.rb +37 -0
- data/spec/hashematics/object_interface_spec.rb +42 -0
- data/spec/hashematics/record_set_spec.rb +24 -0
- data/spec/hashematics/record_spec.rb +49 -0
- data/spec/hashematics/type_spec.rb +104 -0
- data/spec/spec_helper.rb +42 -0
- metadata +211 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A group is a node in a tree structure connected to other groups through the children
|
12
|
+
# attribute. A group essentially represents an object within the object graph and its:
|
13
|
+
# 1. Category (index) for the parent to use as a lookup
|
14
|
+
# 2. Type that describes the object properties, field mapping, etc.
|
15
|
+
class Group
|
16
|
+
attr_reader :category, :name, :type
|
17
|
+
|
18
|
+
def initialize(category:, children:, name:, type:)
|
19
|
+
@category = category
|
20
|
+
@child_dictionary = Dictionary.new.add(children, &:name)
|
21
|
+
@name = name
|
22
|
+
@type = type
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(record)
|
28
|
+
category.add(record)
|
29
|
+
|
30
|
+
child_dictionary.each { |c| c.add(record) }
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def children
|
36
|
+
child_dictionary.map(&:name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit(parent_record = nil)
|
40
|
+
category.records(parent_record).map do |record|
|
41
|
+
Visitor.new(group: self, record: record)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_children(name, parent_record = nil)
|
46
|
+
child_group(name)&.visit(parent_record) || []
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_reader :child_dictionary
|
52
|
+
|
53
|
+
def child_group(group_name)
|
54
|
+
child_dictionary.get(group_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 'digest'
|
11
|
+
require 'forwardable'
|
12
|
+
require 'ostruct'
|
13
|
+
|
14
|
+
require_relative 'category'
|
15
|
+
require_relative 'configuration'
|
16
|
+
require_relative 'dictionary'
|
17
|
+
require_relative 'graph'
|
18
|
+
require_relative 'group'
|
19
|
+
require_relative 'key'
|
20
|
+
require_relative 'id'
|
21
|
+
require_relative 'object_interface'
|
22
|
+
require_relative 'record'
|
23
|
+
require_relative 'record_set'
|
24
|
+
require_relative 'type'
|
25
|
+
require_relative 'visitor'
|
26
|
+
|
27
|
+
# Top-level API syntactic sugar that holds the common library use(s).
|
28
|
+
module Hashematics
|
29
|
+
class << self
|
30
|
+
def graph(config: {}, rows: [])
|
31
|
+
groups = ::Hashematics::Configuration.new(config).groups
|
32
|
+
|
33
|
+
::Hashematics::Graph.new(groups).add(rows)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# An ID is just like a Key except its value is digested (hashed). The main rationale for this
|
12
|
+
# is ID's also contains user data, which could be unbound data, which could potentially
|
13
|
+
# consume lots of memory. To limit this, we digest it.
|
14
|
+
class Id < Key
|
15
|
+
class << self
|
16
|
+
# This method is class-level to expose the underlying hashing algorithm used.
|
17
|
+
def digest(val = '')
|
18
|
+
# MD5 was chosen for its speed, it was not chosen for security.
|
19
|
+
Digest::MD5.hexdigest(val)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def make_value
|
26
|
+
self.class.digest(parts.map(&:to_s).join(SEPARATOR))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A Key is a unique identifier and can be used for hash keys, comparison, etc.
|
12
|
+
# Essentially it is a joined and hashed list of strings.
|
13
|
+
class Key
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# This class-level method allows for the caching/memoization of Key objects already
|
18
|
+
# allocated. Since Key objects will have such a high instantiation count with
|
19
|
+
# the potential of a lof of re-use, it makes sense to try to be a bit more
|
20
|
+
# memory-optimized here.
|
21
|
+
def get(parts = [])
|
22
|
+
return parts if parts.is_a?(self)
|
23
|
+
|
24
|
+
keys[parts] ||= new(parts)
|
25
|
+
end
|
26
|
+
alias default get
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def keys
|
31
|
+
@keys ||= {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
SEPARATOR = '::'
|
36
|
+
|
37
|
+
private_constant :SEPARATOR
|
38
|
+
|
39
|
+
def_delegators :parts, :each_with_object, :map, :any?
|
40
|
+
|
41
|
+
attr_reader :parts, :value
|
42
|
+
|
43
|
+
def initialize(parts = [])
|
44
|
+
@parts = Array(parts)
|
45
|
+
@value = make_value
|
46
|
+
|
47
|
+
freeze
|
48
|
+
end
|
49
|
+
|
50
|
+
# We can compare a Key object to a non-Key object since its constructor is rather pliable.
|
51
|
+
# This means we can do things like this:
|
52
|
+
# - Key.make(['id', :name]) == ['id', 'name']
|
53
|
+
# - Key.make(:id) == 'id'
|
54
|
+
# - Key.make(['id']) == :id
|
55
|
+
# Those are all equivalent and should return true.
|
56
|
+
def eql?(other)
|
57
|
+
return eql?(self.class.get(other)) unless other.is_a?(self.class)
|
58
|
+
|
59
|
+
value == other.value
|
60
|
+
end
|
61
|
+
|
62
|
+
def ==(other)
|
63
|
+
eql?(other)
|
64
|
+
end
|
65
|
+
|
66
|
+
def hash
|
67
|
+
value.hash
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def make_value
|
73
|
+
parts.map(&:to_s).join(SEPARATOR)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# ObjectInterface allows us to interact with external objects in a more standardized manner.
|
12
|
+
# For example: configuration and objects passed into the module can be a little more liberal
|
13
|
+
# in their specific types and key types.
|
14
|
+
class ObjectInterface
|
15
|
+
class << self
|
16
|
+
def get(object, key)
|
17
|
+
if object.is_a?(Hash)
|
18
|
+
indifferent_hash_get(object, key)
|
19
|
+
elsif object.respond_to?(key)
|
20
|
+
object.send(key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def indifferent_hash_get(hash, key)
|
27
|
+
if hash.key?(key.to_s)
|
28
|
+
hash[key.to_s]
|
29
|
+
elsif hash.key?(key.to_s.to_sym)
|
30
|
+
hash[key.to_s.to_sym]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A Record object is composed of an inner object (most likely a hash) and provides extra
|
12
|
+
# methods for the library.
|
13
|
+
class Record
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def_delegators :data, :keys, :hash
|
17
|
+
|
18
|
+
attr_reader :data
|
19
|
+
|
20
|
+
def initialize(data = {})
|
21
|
+
@data = data
|
22
|
+
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def id?(key)
|
27
|
+
Key.get(key).any? { |p| data[p].to_s.length.positive? }
|
28
|
+
end
|
29
|
+
|
30
|
+
def id(key)
|
31
|
+
Id.get(id_parts(key))
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](key)
|
35
|
+
ObjectInterface.get(data, key)
|
36
|
+
end
|
37
|
+
|
38
|
+
# This should allow for Record objects to be compared to:
|
39
|
+
# - Other Record objects
|
40
|
+
# - Other data payload objects (most likely Hash objects)
|
41
|
+
def eql?(other)
|
42
|
+
return eql?(self.class.new(other)) unless other.is_a?(self.class)
|
43
|
+
|
44
|
+
data == other.data
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
eql?(other)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def id_parts(key)
|
54
|
+
Key.get(key).each_with_object([]) do |p, arr|
|
55
|
+
arr << p
|
56
|
+
arr << data[p]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A RecordSet creates Records and maintains a master list of Records.
|
12
|
+
class RecordSet
|
13
|
+
attr_reader :records
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@records = []
|
17
|
+
|
18
|
+
freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def rows
|
22
|
+
records.map(&:data)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(object)
|
26
|
+
Record.new(object).tap { |r| records << r }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A Type defines an object.
|
12
|
+
class Type
|
13
|
+
class << self
|
14
|
+
def null_type
|
15
|
+
@null_type ||= new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
HASH_VALUE = 'hash'
|
20
|
+
OPEN_STRUCT_VALUE = 'open_struct'
|
21
|
+
|
22
|
+
attr_reader :name, :object_class, :properties
|
23
|
+
|
24
|
+
def initialize(name: '', properties: nil, object_class: nil)
|
25
|
+
@name = name
|
26
|
+
@properties = make_properties(properties)
|
27
|
+
@object_class = object_class || HASH_VALUE
|
28
|
+
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def convert(object, child_hash = {})
|
33
|
+
make_object(to_hash(object).merge(child_hash))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def to_hash(object)
|
39
|
+
(properties || default_properties(object)).map do |property, key|
|
40
|
+
[property, ObjectInterface.get(object, key)]
|
41
|
+
end.to_h
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_properties(object)
|
45
|
+
if object.respond_to?(:keys)
|
46
|
+
make_properties(object.keys)
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def make_object(hash)
|
53
|
+
if object_class.to_s == HASH_VALUE
|
54
|
+
hash
|
55
|
+
elsif object_class.to_s == OPEN_STRUCT_VALUE
|
56
|
+
OpenStruct.new(hash)
|
57
|
+
elsif object_class.is_a?(Proc)
|
58
|
+
object_class.call(hash)
|
59
|
+
else
|
60
|
+
object_class.new(hash)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def make_properties(val)
|
65
|
+
if val.is_a?(Array) || val.is_a?(String) || val.is_a?(Symbol)
|
66
|
+
Array(val).map { |v| [v, v] }.to_h
|
67
|
+
else
|
68
|
+
val
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
VERSION = '1.0.0'
|
12
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2019-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 Hashematics
|
11
|
+
# A Visitor is a Record found in the context of a Group. When traversing the object
|
12
|
+
# graph (group tree), it will provide these Visitor objects instead of Record objects
|
13
|
+
# that allows you to view the Record in the context of the graph, while a Record is more of just
|
14
|
+
# the raw payload provided by the initial flat data set.
|
15
|
+
class Visitor
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
def_delegators :group, :children, :type
|
19
|
+
|
20
|
+
attr_reader :group, :record
|
21
|
+
|
22
|
+
def initialize(group:, record:)
|
23
|
+
@group = group
|
24
|
+
@record = record
|
25
|
+
|
26
|
+
freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
def data(include_children = false)
|
30
|
+
child_hash = include_children ? make_child_hash : {}
|
31
|
+
|
32
|
+
type.convert(record.data, child_hash)
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit(name)
|
36
|
+
group.visit_children(name, record)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def make_child_hash
|
42
|
+
children.map do |name|
|
43
|
+
[
|
44
|
+
name,
|
45
|
+
visit(name).map { |v| v.data(true) }
|
46
|
+
]
|
47
|
+
end.to_h
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|