hashematics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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