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.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +104 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +220 -0
- data/airspace.gemspec +33 -0
- data/bin/console +15 -0
- data/lib/airspace.rb +10 -0
- data/lib/airspace/airspace.rb +50 -0
- data/lib/airspace/chunker.rb +47 -0
- data/lib/airspace/dataset.rb +79 -0
- data/lib/airspace/has_metadata.rb +24 -0
- data/lib/airspace/info_keys.rb +17 -0
- data/lib/airspace/key.rb +35 -0
- data/lib/airspace/metadata.rb +44 -0
- data/lib/airspace/reader.rb +92 -0
- data/lib/airspace/serializer.rb +42 -0
- data/lib/airspace/store.rb +115 -0
- data/lib/airspace/version.rb +12 -0
- data/spec/airspace/airspace_spec.rb +177 -0
- data/spec/airspace/chunker_spec.rb +111 -0
- data/spec/airspace/store_spec.rb +21 -0
- data/spec/spec_helper.rb +33 -0
- metadata +190 -0
data/bin/console
ADDED
@@ -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
|
data/lib/airspace.rb
ADDED
@@ -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
|
data/lib/airspace/key.rb
ADDED
@@ -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
|