airspace 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,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'airspace'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start
@@ -0,0 +1,10 @@
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_relative 'airspace/airspace'
@@ -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
+ require 'forwardable'
11
+ require 'json'
12
+ require 'securerandom'
13
+ require 'time'
14
+
15
+ require_relative 'key'
16
+ require_relative 'info_keys'
17
+ require_relative 'chunker'
18
+ require_relative 'metadata'
19
+ require_relative 'has_metadata'
20
+ require_relative 'dataset'
21
+ require_relative 'serializer'
22
+ require_relative 'reader'
23
+ require_relative 'store'
24
+
25
+ # Top-level namespace for primary public API.
26
+ module Airspace
27
+ class << self
28
+ def set(client, id: nil, data: {}, pages: [], options: {})
29
+ ::Airspace::Dataset.new(
30
+ client,
31
+ id: id,
32
+ data: data,
33
+ pages: pages,
34
+ options: options
35
+ ).save.id
36
+ end
37
+
38
+ def get(client, id, options: {})
39
+ ::Airspace::Reader.find_by_id(client, id, options: options)
40
+ end
41
+
42
+ def del(client, id, options: {})
43
+ reader = get(client, id, options: options)
44
+
45
+ return false unless reader
46
+
47
+ reader.delete
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
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 Airspace
11
+ # Chunking here is defined as: taking an array of pages and grouping them into groups of pages
12
+ # (chunks) in order to find a middle-ground of server-side page and entire dataset fetches.
13
+ class Chunker
14
+ Location = Struct.new(:chunk_index, :page_index)
15
+ Chunk = Struct.new(:chunk_index, :page_index_start, :page_index_end)
16
+
17
+ attr_reader :pages_per_chunk
18
+
19
+ def initialize(pages_per_chunk)
20
+ raise ArgumentError unless pages_per_chunk.positive?
21
+
22
+ @pages_per_chunk = pages_per_chunk
23
+ end
24
+
25
+ def count(page_total)
26
+ (page_total / pages_per_chunk.to_f).ceil
27
+ end
28
+
29
+ def each(page_total)
30
+ return enum_for(:each, page_total) unless block_given?
31
+
32
+ (0...count(page_total)).each do |chunk_index|
33
+ page_index_start = chunk_index * pages_per_chunk
34
+ page_index_end = page_index_start + pages_per_chunk - 1
35
+
36
+ yield Chunk.new(chunk_index, page_index_start, page_index_end)
37
+ end
38
+ end
39
+
40
+ def locate(index)
41
+ chunk_index = (index / pages_per_chunk.to_f).floor
42
+ page_index = index % pages_per_chunk
43
+
44
+ Location.new(chunk_index, page_index)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,79 @@
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 Airspace
11
+ # This is the main input class that can persist data.
12
+ class Dataset
13
+ include ::Airspace::InfoKeys
14
+ include ::Airspace::HasMetadata
15
+
16
+ attr_reader :client,
17
+ :data,
18
+ :id,
19
+ :pages,
20
+ :prefix,
21
+ :serializer
22
+
23
+ def initialize(client, id: nil, data: {}, pages: [], options: {})
24
+ raise ArgumentError, 'client is required' unless client
25
+
26
+ @client = client
27
+ @data = data || {}
28
+ @id = id || SecureRandom.uuid
29
+ @pages = pages || []
30
+ @prefix = options[:prefix].to_s
31
+ @serializer = options[:serializer] || ::Airspace::Serializer.new
32
+
33
+ @metadata = ::Airspace::Metadata.new(
34
+ expires_in_seconds: options[:expires_in_seconds],
35
+ page_count: pages.length,
36
+ pages_per_chunk: options[:pages_per_chunk]
37
+ )
38
+
39
+ freeze
40
+ end
41
+
42
+ def save
43
+ store.persist(key, info_hash, chunks, expires_in_seconds)
44
+
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def info_hash
51
+ {}.tap do |hash|
52
+ hash[DATA_KEY] = serializer.serialize_data(data)
53
+ hash[METADATA_KEY] = metadata.to_json
54
+ end
55
+ end
56
+
57
+ def chunks
58
+ chunks = []
59
+
60
+ chunker.each(page_count) do |chunk|
61
+ chunk_data = pages[chunk.page_index_start..chunk.page_index_end]
62
+
63
+ chunks << chunk_data.map do |page|
64
+ page.map { |row| serializer.serialize_row(row) }
65
+ end
66
+ end
67
+
68
+ chunks
69
+ end
70
+
71
+ def store
72
+ ::Airspace::Store.new(client)
73
+ end
74
+
75
+ def key
76
+ ::Airspace::Key.new(id, prefix: prefix)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,24 @@
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 Airspace
11
+ # This mix-in allows for classes to be composed of a Metadata instance
12
+ module HasMetadata
13
+ extend Forwardable
14
+
15
+ attr_reader :metadata
16
+
17
+ def_delegators :metadata,
18
+ :chunk_count,
19
+ :chunker,
20
+ :expires_in_seconds,
21
+ :page_count,
22
+ :pages_per_chunk
23
+ end
24
+ end
@@ -0,0 +1,17 @@
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 Airspace
11
+ # Holds shared data among how to internally access data.
12
+ module InfoKeys
13
+ DATA_KEY = 'd'
14
+ METADATA_KEY = 'm'
15
+ SEPARATOR_CHAR = ':'
16
+ end
17
+ 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 Airspace
11
+ # This class understands how to build keys and subkeys for storing data inside Redis.
12
+ class Key
13
+ SEPARATOR_CHAR = ':'
14
+
15
+ private_constant :SEPARATOR_CHAR
16
+
17
+ attr_reader :id, :prefix
18
+
19
+ def initialize(id, prefix: '')
20
+ @id = id.to_s
21
+ @prefix = prefix.to_s
22
+ end
23
+
24
+ def root
25
+ return id if prefix.empty?
26
+
27
+ [prefix, id].join(SEPARATOR_CHAR)
28
+ end
29
+ alias to_s root
30
+
31
+ def chunk(index)
32
+ [root, index].join(SEPARATOR_CHAR)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
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 Airspace
11
+ # Metdata is 'data about a dataset.' These are key pieces of information we need to store with
12
+ # the data then later retrieve with the data.
13
+ class Metadata
14
+ DEFAULT_PAGES_PER_CHUNK = 5
15
+
16
+ attr_reader :expires_in_seconds,
17
+ :page_count,
18
+ :pages_per_chunk
19
+
20
+ def initialize(expires_in_seconds: nil, page_count: 0, pages_per_chunk: DEFAULT_PAGES_PER_CHUNK)
21
+ @expires_in_seconds = expires_in_seconds ? expires_in_seconds.to_i : nil
22
+ @page_count = page_count.to_i
23
+ @pages_per_chunk = pages_per_chunk ? pages_per_chunk.to_i : DEFAULT_PAGES_PER_CHUNK
24
+
25
+ freeze
26
+ end
27
+
28
+ def chunker
29
+ ::Airspace::Chunker.new(pages_per_chunk)
30
+ end
31
+
32
+ def chunk_count
33
+ chunker.count(page_count)
34
+ end
35
+
36
+ def to_json
37
+ {
38
+ expires_in_seconds: expires_in_seconds,
39
+ page_count: page_count,
40
+ pages_per_chunk: pages_per_chunk
41
+ }.to_json
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,92 @@
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 Airspace
11
+ # This is the main class that knows how to fetch and interpret the dataset.
12
+ # It is optimized for chunking/paging and allows you to only pull back specific
13
+ # pages (if desired.)
14
+ class Reader
15
+ extend Forwardable
16
+ extend ::Airspace::InfoKeys
17
+ include ::Airspace::HasMetadata
18
+
19
+ class << self
20
+ def find_by_id(client, id, options: {})
21
+ key = ::Airspace::Key.new(id, prefix: options[:prefix])
22
+ serializer = options[:serializer] || ::Airspace::Serializer.new
23
+ hash = fetch_and_transform(client, key, serializer)
24
+ return nil unless hash
25
+
26
+ metadata_args = hash[METADATA_KEY].map { |k, v| [k.to_sym, v] }.to_h
27
+
28
+ new(
29
+ client,
30
+ data: hash[DATA_KEY],
31
+ key: key,
32
+ metadata: ::Airspace::Metadata.new(metadata_args),
33
+ serializer: serializer
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def fetch_and_transform(client, key, serializer)
40
+ hash = ::Airspace::Store.new(client).retrieve(key)
41
+ return nil unless hash
42
+
43
+ {}.tap do |h|
44
+ h[DATA_KEY] = serializer.deserialize_data(hash[DATA_KEY])
45
+ h[METADATA_KEY] = JSON.parse(hash[METADATA_KEY])
46
+ end
47
+ end
48
+ end
49
+
50
+ attr_reader :client,
51
+ :data,
52
+ :key,
53
+ :metadata,
54
+ :serializer
55
+
56
+ def_delegators :key, :id
57
+
58
+ def initialize(client, data:, key:, metadata:, serializer:)
59
+ @client = client
60
+ @key = key
61
+ @data = data
62
+ @metadata = metadata
63
+ @serializer = serializer
64
+
65
+ freeze
66
+ end
67
+
68
+ def pages
69
+ store.chunks(key, chunk_count).map do |chunk|
70
+ chunk.map { |r| serializer.deserialize_row(r) }
71
+ end
72
+ end
73
+
74
+ def page(number)
75
+ page_index = number - 1
76
+ location = chunker.locate(page_index)
77
+ chunk = store.chunk(key, location.chunk_index)
78
+
79
+ chunk[location.page_index].map { |r| serializer.deserialize_row(r) }
80
+ end
81
+
82
+ def delete
83
+ store.delete(key, page_count)
84
+ end
85
+
86
+ private
87
+
88
+ def store
89
+ ::Airspace::Store.new(client)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,42 @@
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 Airspace
11
+ # This class dictates how data is stored and retrieved. You can subclass this and
12
+ # change its implementation that suits your overall data/space requirements.
13
+ class Serializer
14
+ def serialize_data(obj)
15
+ json_serialize(obj)
16
+ end
17
+
18
+ def deserialize_data(json)
19
+ json_deserialize(json)
20
+ end
21
+
22
+ def serialize_row(obj)
23
+ json_serialize(obj)
24
+ end
25
+
26
+ def deserialize_row(json)
27
+ json_deserialize(json)
28
+ end
29
+
30
+ private
31
+
32
+ def json_deserialize(json)
33
+ return nil unless json
34
+
35
+ JSON.parse(json)
36
+ end
37
+
38
+ def json_serialize(obj)
39
+ obj.to_json
40
+ end
41
+ end
42
+ end