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