destruct 0.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/.buildkite/pipeline.yml +9 -0
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/Dockerfile +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +20 -0
- data/README.md +683 -0
- data/bin/rubocop +29 -0
- data/bin/test +64 -0
- data/destruct.gemspec +15 -0
- data/docker-compose.yml +21 -0
- data/lib/destruct.rb +39 -0
- data/lib/destruct/dsl.rb +32 -0
- data/lib/destruct/hash.rb +32 -0
- data/lib/destruct/object.rb +12 -0
- data/lib/destruct/resolver.rb +41 -0
- data/spec/destruct/hash_spec.rb +94 -0
- data/spec/destruct/object_spec.rb +42 -0
- data/spec/destruct/resolver_spec.rb +31 -0
- data/spec/destruct_spec.rb +68 -0
- data/spec/spec_helper.rb +15 -0
- metadata +76 -0
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -eo pipefail
|
4
|
+
|
5
|
+
echo -e "--- \033[33mWhat does this command do?\033[0m"
|
6
|
+
echo
|
7
|
+
echo " This script runs our LendingHome platform wide rubocop Docker image"
|
8
|
+
echo " for the gem or service mounted at /app. It uses a configuration file"
|
9
|
+
echo " that has been added directly into the rubocop Docker image itself. This"
|
10
|
+
echo " configuration lives in the lendinghome-dockerfiles repository."
|
11
|
+
echo
|
12
|
+
echo " Check out the documentation on rubocop for more information!"
|
13
|
+
echo
|
14
|
+
echo " * https://github.com/bbatsov/rubocop"
|
15
|
+
echo " * https://github.com/bbatsov/ruby-style-guide"
|
16
|
+
echo
|
17
|
+
|
18
|
+
if [ -v REBUILD_CACHE ]; then
|
19
|
+
echo "--- :docker: Pulling latest docker image"
|
20
|
+
docker-compose pull rubocop
|
21
|
+
|
22
|
+
echo "--- :recycle: Updating build cache"
|
23
|
+
echo "The build was triggered with REBUILD_CACHE"
|
24
|
+
echo "Skipping the remaining build steps"
|
25
|
+
else
|
26
|
+
echo "--- :docker: Starting docker"
|
27
|
+
docker-compose pull rubocop
|
28
|
+
docker-compose run rubocop $@
|
29
|
+
fi
|
data/bin/test
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -eo pipefail
|
4
|
+
|
5
|
+
if [[ -z "$DOCKER" ]]; then
|
6
|
+
echo "--- :buildkite: Building destruct"
|
7
|
+
buildkite-agent --version
|
8
|
+
|
9
|
+
echo "branch: $BUILDKITE_BRANCH"
|
10
|
+
echo "commit: $BUILDKITE_COMMIT"
|
11
|
+
echo "image: $DOCKER_IMAGE"
|
12
|
+
|
13
|
+
if [ -v REBUILD_CACHE ]; then
|
14
|
+
echo "--- :minidisc: Pulling images"
|
15
|
+
docker-compose pull --ignore-pull-failures
|
16
|
+
fi
|
17
|
+
|
18
|
+
echo "--- :docker: Starting docker"
|
19
|
+
docker --version
|
20
|
+
docker-compose --version
|
21
|
+
|
22
|
+
echo "Building $DOCKER_IMAGE"
|
23
|
+
docker-compose run app /app/bin/test $@
|
24
|
+
|
25
|
+
if [ -v REBUILD_CACHE ]; then
|
26
|
+
echo "--- :recycle: Updating build cache"
|
27
|
+
echo "The build was triggered with REBUILD_CACHE"
|
28
|
+
|
29
|
+
echo "Generating cache.tar.lz4"
|
30
|
+
tar -c --use-compress-program=lz4 -f cache.tar.lz4 "$BUILD_CACHE" \
|
31
|
+
|| echo "Ignoring permission denied failures"
|
32
|
+
|
33
|
+
[ -v BUILDKITE ] && buildkite-agent meta-data set "rebuild-cache" "1"
|
34
|
+
fi
|
35
|
+
|
36
|
+
exit 0
|
37
|
+
fi
|
38
|
+
|
39
|
+
echo "--- :terminal: Loading environment"
|
40
|
+
echo "bash: `bash --version | head -1`"
|
41
|
+
echo "git: `git --version`"
|
42
|
+
echo "imagemagick: `identify --version | head -1`"
|
43
|
+
echo "java: `java -version 2>&1 | head -1`"
|
44
|
+
echo "os: `cat /etc/issue | head -1`"
|
45
|
+
echo "phantomjs: `phantomjs --version`"
|
46
|
+
|
47
|
+
echo "--- :ruby: Installing ruby"
|
48
|
+
rbenv install --skip-existing
|
49
|
+
ruby --version
|
50
|
+
rbenv --version
|
51
|
+
|
52
|
+
echo "--- :rubygems: Installing ruby gems"
|
53
|
+
echo "gem $(gem --version)"
|
54
|
+
bundler --version
|
55
|
+
cpus=$(nproc 2>/dev/null || echo 1)
|
56
|
+
[[ $cpus -eq 1 ]] && jobs=1 || jobs=$((cpus - 1))
|
57
|
+
options="--path vendor/bundle --jobs $jobs --retry 3 --frozen --no-cache --no-prune"
|
58
|
+
bundle check || bundle install $options
|
59
|
+
|
60
|
+
if [[ -z "$REBUILD_CACHE" ]]; then
|
61
|
+
echo "+++ :rspec: Running rspec tests"
|
62
|
+
echo "rspec $(bundle exec rspec --version)"
|
63
|
+
bundle exec rspec $@
|
64
|
+
fi
|
data/destruct.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.author = "LendingHome"
|
3
|
+
s.email = "github@lendinghome.com"
|
4
|
+
s.extra_rdoc_files = %w(LICENSE)
|
5
|
+
s.files = `git ls-files`.split("\n")
|
6
|
+
s.homepage = "https://github.com/lendinghome/destruct"
|
7
|
+
s.license = "MIT"
|
8
|
+
s.name = "destruct"
|
9
|
+
s.rdoc_options = %w(--charset=UTF-8 --inline-source --line-numbers --main README.md)
|
10
|
+
s.require_paths = %w(lib)
|
11
|
+
s.required_ruby_version = ">= 2.3.0"
|
12
|
+
s.summary = "ES6 style object destructuring in Ruby"
|
13
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
14
|
+
s.version = "0.0.0"
|
15
|
+
end
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
version: "2"
|
2
|
+
|
3
|
+
services:
|
4
|
+
app:
|
5
|
+
build:
|
6
|
+
context: "."
|
7
|
+
image: $DOCKER_IMAGE
|
8
|
+
environment:
|
9
|
+
- CI
|
10
|
+
- DOCKER=true
|
11
|
+
- REBUILD_CACHE
|
12
|
+
volumes:
|
13
|
+
- .:/app
|
14
|
+
- $BUILD_CACHE/bundler:/app/vendor/bundle
|
15
|
+
|
16
|
+
rubocop:
|
17
|
+
image: lendinghome/rubocop
|
18
|
+
environment:
|
19
|
+
- RUBOCOP_VERSION=2.3
|
20
|
+
volumes:
|
21
|
+
- .:/app
|
data/lib/destruct.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative "destruct/dsl"
|
2
|
+
require_relative "destruct/hash"
|
3
|
+
require_relative "destruct/object"
|
4
|
+
require_relative "destruct/resolver"
|
5
|
+
|
6
|
+
Object.send(:include, Destruct::Object)
|
7
|
+
|
8
|
+
class Destruct
|
9
|
+
GEMSPEC = File.expand_path("../../destruct.gemspec", __FILE__)
|
10
|
+
VERSION = Gem::Specification.load(GEMSPEC).version.to_s
|
11
|
+
|
12
|
+
def initialize(object, *paths, &block)
|
13
|
+
@object = object
|
14
|
+
@paths = paths
|
15
|
+
@block = block || proc { }
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
paths.each_with_object(Hash.new) do |path, hash|
|
20
|
+
resolve(path).each do |key, value|
|
21
|
+
hash[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def dsl
|
29
|
+
DSL.new(&@block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def paths
|
33
|
+
@paths + dsl.paths
|
34
|
+
end
|
35
|
+
|
36
|
+
def resolve(path)
|
37
|
+
Resolver.new(@object, path).to_h
|
38
|
+
end
|
39
|
+
end
|
data/lib/destruct/dsl.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
class Destruct
|
2
|
+
class DSL
|
3
|
+
def initialize(&block)
|
4
|
+
@paths = []
|
5
|
+
instance_eval(&block) if block
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](path)
|
9
|
+
self.class.new.tap do |dsl|
|
10
|
+
@paths << [path, dsl]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def paths
|
15
|
+
@paths.each_with_object([]) do |(key, dsl), paths|
|
16
|
+
nested = dsl.paths
|
17
|
+
paths << [key] if nested.empty?
|
18
|
+
nested.each { |path| paths << path.unshift(key) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def method_missing(method, *)
|
25
|
+
self[method]
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(method, *)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Destruct
|
2
|
+
class Hash < ::Hash
|
3
|
+
def [](key)
|
4
|
+
super(key) || super(key.to_s) || to_ary[key]
|
5
|
+
rescue TypeError
|
6
|
+
nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(key, value)
|
10
|
+
to_ary << value
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def key?(key)
|
15
|
+
super(key) || super(key.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_ary
|
19
|
+
@to_ary ||= values
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def method_missing(method, *)
|
25
|
+
key?(method) ? self[method] : super
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(method, *)
|
29
|
+
key?(method) || super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Destruct
|
2
|
+
class Resolver
|
3
|
+
def initialize(object, path)
|
4
|
+
@object = object
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_h
|
9
|
+
case @path
|
10
|
+
when Array
|
11
|
+
{ @path.last => @object.dig(*@path) }
|
12
|
+
when ::Hash
|
13
|
+
resolve_recursively
|
14
|
+
else
|
15
|
+
self.class.new(@object, paths).to_h
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def paths
|
22
|
+
Array(@path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def resolve(key, values)
|
26
|
+
@object.dig(key).tap do |object|
|
27
|
+
Array(values).each do |value|
|
28
|
+
yield self.class.new(object, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def resolve_recursively
|
34
|
+
@path.each_with_object({}) do |(key, values), hash|
|
35
|
+
resolve(key, values) do |resolver|
|
36
|
+
hash.update(resolver.to_h)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
RSpec.describe Destruct::Hash do
|
2
|
+
subject { described_class.new.replace(hash) }
|
3
|
+
|
4
|
+
let(:hash) do
|
5
|
+
{
|
6
|
+
status: 200,
|
7
|
+
"multi word" => "example",
|
8
|
+
"test" => "string"
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#[]" do
|
13
|
+
it "looks up by key" do
|
14
|
+
expect(subject[:status]).to eq(200)
|
15
|
+
expect(subject["test"]).to eq("string")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "looks up by stringified key" do
|
19
|
+
expect(subject[:test]).to eq("string")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "looks up by multi-word key" do
|
23
|
+
expect(subject["multi word"]).to eq("example")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "looks up by index key" do
|
27
|
+
expect(subject[0]).to eq(200)
|
28
|
+
expect(subject[1]).to eq("example")
|
29
|
+
expect(subject[2]).to eq("string")
|
30
|
+
expect(subject[-1]).to eq("string")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns nil when key is not found" do
|
34
|
+
expect(subject[:invalid]).to be_nil
|
35
|
+
expect(subject["invalid"]).to be_nil
|
36
|
+
expect(subject[99]).to be_nil
|
37
|
+
expect(subject[{}]).to be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#key?" do
|
42
|
+
it "looks up by key" do
|
43
|
+
expect(subject).to be_key(:status)
|
44
|
+
expect(subject).to be_key("test")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "looks up by stringified key" do
|
48
|
+
expect(subject).to be_key(:test)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "looks up by multi-word key" do
|
52
|
+
expect(subject).to be_key("multi word")
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns false when key is not found" do
|
56
|
+
expect(subject).not_to be_key(:invalid)
|
57
|
+
expect(subject).not_to be_key("invalid")
|
58
|
+
expect(subject).not_to be_key(1)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#method_missing" do
|
63
|
+
it "resolves symbol keys" do
|
64
|
+
expect(subject.status).to eq(200)
|
65
|
+
expect(subject.test).to eq("string")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "resolves string keys" do
|
69
|
+
expect(subject.test).to eq("string")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "raises NoMethodError otherwise" do
|
73
|
+
expect { subject.invalid }.to raise_error(NoMethodError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#respond_to?" do
|
78
|
+
it "returns true if keys exist" do
|
79
|
+
expect(subject).to be_respond_to(:status)
|
80
|
+
expect(subject).to be_respond_to(:test)
|
81
|
+
expect(subject).to be_respond_to("test")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns false if keys do not exist" do
|
85
|
+
expect(subject).not_to be_respond_to(:invalid)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#to_ary" do
|
90
|
+
it "returns the subject values" do
|
91
|
+
expect(subject.to_ary).to eq(subject.values)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
RSpec.describe Destruct::Object do
|
2
|
+
subject { "test" }
|
3
|
+
|
4
|
+
describe "#destruct" do
|
5
|
+
it "is defined on all objects" do
|
6
|
+
expect(subject).to respond_to(:destruct)
|
7
|
+
expect(Object).to respond_to(:destruct)
|
8
|
+
expect(Object.new).to respond_to(:destruct)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#dig" do
|
13
|
+
it "resolves nested methods" do
|
14
|
+
actual = subject.dig(:upcase, :reverse)
|
15
|
+
expect(actual).to eq("TSET")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "resolves nested hashes" do
|
19
|
+
struct = Struct.new(:hash)
|
20
|
+
object = struct.new(one: { two: 2 })
|
21
|
+
actual = object.dig(:hash, :one, :two)
|
22
|
+
expect(actual).to eq(2)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "resolves nested arrays" do
|
26
|
+
struct = Struct.new(:array)
|
27
|
+
object = struct.new([1, [2, 3]])
|
28
|
+
actual = object.dig(:array, 1, 0)
|
29
|
+
expect(actual).to eq(2)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns nil for undefined methods" do
|
33
|
+
actual = subject.dig(:upcase, :undefined, :reverse)
|
34
|
+
expect(actual).to be_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns nil for invalid methods" do
|
38
|
+
actual = subject.dig(:upcase, "invalid method", :reverse)
|
39
|
+
expect(actual).to be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
RSpec.describe Destruct::Resolver do
|
2
|
+
subject { described_class.new(object, path) }
|
3
|
+
|
4
|
+
let(:object) { "test" }
|
5
|
+
|
6
|
+
describe "#to_h" do
|
7
|
+
let(:path) { :upcase }
|
8
|
+
|
9
|
+
context "with object argument" do
|
10
|
+
it "parses correctly" do
|
11
|
+
expect(subject.to_h).to eq(upcase: "TEST")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with array argument" do
|
16
|
+
let(:path) { %i(upcase reverse) }
|
17
|
+
|
18
|
+
it "parses correctly" do
|
19
|
+
expect(subject.to_h).to eq(reverse: "TSET")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "with hash argument" do
|
24
|
+
let(:path) { { upcase: :reverse } }
|
25
|
+
|
26
|
+
it "parses correctly" do
|
27
|
+
expect(subject.to_h).to eq(reverse: "TSET")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|