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.
@@ -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