exa 0.1.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/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.md +33 -0
- data/Rakefile +31 -0
- data/bin/exa +20 -0
- data/exa.gemspec +60 -0
- data/features/.gitkeep +0 -0
- data/features/exa.feature +1 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/step_definitions/exa_steps.rb +1 -0
- data/gemspec.yml +16 -0
- data/lib/exa.rb +85 -0
- data/lib/exa/shell.rb +107 -0
- data/lib/exa/tree_node.rb +159 -0
- data/lib/exa/version.rb +4 -0
- data/lib/exa/visitor.rb +100 -0
- data/spec/exa_spec.rb +121 -0
- data/spec/spec_helper.rb +4 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f27cfd470e0649009f1628ac416129e508b9f892
|
4
|
+
data.tar.gz: de0b8c876be5db9d1aa5e51f139f675d0fef6fbf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84260a3a82086953e2e99ff908f370609ddbe5a60bf7567f646939ae71137477c7e00b5c29359d8339a458c1cb527dbc1bc14c891b376604c4a5bd78e2bb0737
|
7
|
+
data.tar.gz: 85cca81bd881342d01aa2878127d9469a1f154258285dcefd43794f1b948fdc8398e20a0d3c44d780b3b9069ecf9792842b75491ce43ae655c9337568096b9db
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format documentation
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown --title "exa Documentation" --protected
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Joseph Weissman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# exa
|
2
|
+
|
3
|
+
* [Homepage](https://rubygems.org/gems/exa)
|
4
|
+
* [Documentation](http://rubydoc.info/gems/exa/frames)
|
5
|
+
* [Email](mailto:jweissman1986 at gmail.com)
|
6
|
+
|
7
|
+
[](https://codeclimate.com/github//exa)
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
TODO: Description
|
12
|
+
|
13
|
+
## Features
|
14
|
+
|
15
|
+
## Examples
|
16
|
+
|
17
|
+
require 'exa'
|
18
|
+
|
19
|
+
## Requirements
|
20
|
+
|
21
|
+
## Install
|
22
|
+
|
23
|
+
$ gem install exa
|
24
|
+
|
25
|
+
## Synopsis
|
26
|
+
|
27
|
+
$ exa
|
28
|
+
|
29
|
+
## Copyright
|
30
|
+
|
31
|
+
Copyright (c) 2016 Joseph Weissman
|
32
|
+
|
33
|
+
See {file:LICENSE.txt} for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler/setup'
|
7
|
+
rescue LoadError => e
|
8
|
+
abort e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems/tasks'
|
15
|
+
Gem::Tasks.new
|
16
|
+
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
RSpec::Core::RakeTask.new
|
19
|
+
|
20
|
+
task :test => :spec
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
require 'yard'
|
24
|
+
YARD::Rake::YardocTask.new
|
25
|
+
task :doc => :yard
|
26
|
+
|
27
|
+
require 'cucumber/rake/task'
|
28
|
+
|
29
|
+
Cucumber::Rake::Task.new do |t|
|
30
|
+
t.cucumber_opts = %w[--format pretty]
|
31
|
+
end
|
data/bin/exa
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
root = File.expand_path(File.join(File.dirname(__FILE__),'..'))
|
4
|
+
if File.directory?(File.join(root,'.git'))
|
5
|
+
Dir.chdir(root) do
|
6
|
+
begin
|
7
|
+
require 'bundler/setup'
|
8
|
+
require 'exa'
|
9
|
+
require 'exa/shell'
|
10
|
+
|
11
|
+
Exa::Shell.repl! do |config|
|
12
|
+
config.prompt = -> shell { shell.pastel.bold(" #{shell.pwd.path} $ ") }
|
13
|
+
end
|
14
|
+
rescue LoadError => e
|
15
|
+
warn e.message
|
16
|
+
warn "Run `gem install bundler` to install Bundler"
|
17
|
+
exit(-1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/exa.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gemspec = YAML.load_file('gemspec.yml')
|
7
|
+
|
8
|
+
gem.name = gemspec.fetch('name')
|
9
|
+
gem.version = gemspec.fetch('version') do
|
10
|
+
lib_dir = File.join(File.dirname(__FILE__),'lib')
|
11
|
+
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
|
12
|
+
|
13
|
+
require 'exa/version'
|
14
|
+
Exa::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
gem.summary = gemspec['summary']
|
18
|
+
gem.description = gemspec['description']
|
19
|
+
gem.licenses = Array(gemspec['license'])
|
20
|
+
gem.authors = Array(gemspec['authors'])
|
21
|
+
gem.email = gemspec['email']
|
22
|
+
gem.homepage = gemspec['homepage']
|
23
|
+
|
24
|
+
glob = lambda { |patterns| gem.files & Dir[*patterns] }
|
25
|
+
|
26
|
+
gem.files = `git ls-files`.split($/)
|
27
|
+
gem.files = glob[gemspec['files']] if gemspec['files']
|
28
|
+
|
29
|
+
gem.executables = gemspec.fetch('executables') do
|
30
|
+
glob['bin/*'].map { |path| File.basename(path) }
|
31
|
+
end
|
32
|
+
gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
|
33
|
+
|
34
|
+
gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
|
35
|
+
gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
|
36
|
+
gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
|
37
|
+
|
38
|
+
gem.require_paths = Array(gemspec.fetch('require_paths') {
|
39
|
+
%w[ext lib].select { |dir| File.directory?(dir) }
|
40
|
+
})
|
41
|
+
|
42
|
+
gem.requirements = Array(gemspec['requirements'])
|
43
|
+
gem.required_ruby_version = gemspec['required_ruby_version']
|
44
|
+
gem.required_rubygems_version = gemspec['required_rubygems_version']
|
45
|
+
gem.post_install_message = gemspec['post_install_message']
|
46
|
+
|
47
|
+
split = lambda { |string| string.split(/,\s*/) }
|
48
|
+
|
49
|
+
if gemspec['dependencies']
|
50
|
+
gemspec['dependencies'].each do |name,versions|
|
51
|
+
gem.add_dependency(name,split[versions])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if gemspec['development_dependencies']
|
56
|
+
gemspec['development_dependencies'].each do |name,versions|
|
57
|
+
gem.add_development_dependency(name,split[versions])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/features/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Feature: Blah blah blah
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
@wip
|
data/gemspec.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
name: exa
|
2
|
+
summary: "recursive in-memory algos and data structures"
|
3
|
+
description: "recursive in-memory structures"
|
4
|
+
license: MIT
|
5
|
+
authors: Joseph Weissman
|
6
|
+
email: jweissman1986@gmail.com
|
7
|
+
homepage: https://rubygems.org/gems/exa
|
8
|
+
|
9
|
+
development_dependencies:
|
10
|
+
bundler: ~> 1.10
|
11
|
+
codeclimate-test-reporter: ~> 0.1
|
12
|
+
cucumber: ~> 0.10.2
|
13
|
+
rake: ~> 10.0
|
14
|
+
rspec: ~> 3.0
|
15
|
+
rubygems-tasks: ~> 0.2
|
16
|
+
yard: ~> 0.8
|
data/lib/exa.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'exa/version'
|
2
|
+
require 'exa/tree_node'
|
3
|
+
require 'exa/visitor'
|
4
|
+
|
5
|
+
module Exa
|
6
|
+
class Process
|
7
|
+
def initialize(title)
|
8
|
+
@title = title
|
9
|
+
end
|
10
|
+
|
11
|
+
def register
|
12
|
+
Process.table += [ self ]
|
13
|
+
end
|
14
|
+
|
15
|
+
def unregister
|
16
|
+
Process.table -= [ self ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
register
|
21
|
+
perform!
|
22
|
+
unregister
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.table
|
26
|
+
@table ||= []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Copier < Process
|
31
|
+
def initialize(source, target)
|
32
|
+
@source = source
|
33
|
+
@target = target
|
34
|
+
super("copy #{source} -> #{target}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform!
|
38
|
+
@target.update @source.value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Deleter < Process
|
43
|
+
def initialize(target)
|
44
|
+
@target = target
|
45
|
+
super("delete #{target}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform!
|
49
|
+
@target.parent.remove_child(child_name: @target.name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def remember(path, value)
|
55
|
+
p [ :remember, path: path, value: value ]
|
56
|
+
recall(path).update(value)
|
57
|
+
end
|
58
|
+
alias :[]= :remember
|
59
|
+
|
60
|
+
def recall(path)
|
61
|
+
p [ :recall, path: path ]
|
62
|
+
visitor.seek(path)
|
63
|
+
end
|
64
|
+
alias :[] :recall
|
65
|
+
|
66
|
+
def expand(path)
|
67
|
+
visitor.query(path)
|
68
|
+
end
|
69
|
+
alias :call :expand
|
70
|
+
|
71
|
+
def clean_slate!
|
72
|
+
@root = TreeNode.new(name: '', value: '(root)')
|
73
|
+
# @visitor = Visitor.new(@root)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def visitor
|
78
|
+
Visitor.new(root)
|
79
|
+
end
|
80
|
+
|
81
|
+
def root
|
82
|
+
@root ||= TreeNode.new(name: '', value: '(system root)')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/exa/shell.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'pastel'
|
3
|
+
|
4
|
+
module Exa
|
5
|
+
class ShellConfig
|
6
|
+
attr_accessor :prompt
|
7
|
+
end
|
8
|
+
|
9
|
+
class ShellCommand
|
10
|
+
def initialize(title:,args:)
|
11
|
+
@title = title
|
12
|
+
@args = args
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(shell)
|
16
|
+
case @title
|
17
|
+
when "ls" then
|
18
|
+
if shell.pwd.children.any?
|
19
|
+
shell.print_collection shell.pwd.children.map(&:name)
|
20
|
+
else
|
21
|
+
shell.print_warning "(no children of #{shell.pwd.path})"
|
22
|
+
end
|
23
|
+
when "cd" then
|
24
|
+
target = Exa.expand(@args.first)
|
25
|
+
if target && !target.empty?
|
26
|
+
shell.change_directory target.first
|
27
|
+
else
|
28
|
+
shell.print_warning "Invalid path for cd: #{@args.first}"
|
29
|
+
end
|
30
|
+
when "mkdir" then
|
31
|
+
target = Exa.recall(@args.first)
|
32
|
+
when "pwd" then
|
33
|
+
shell.print_info shell.pwd.path
|
34
|
+
else
|
35
|
+
shell.print_warning "Unknown command: '#@title'"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.extract(string)
|
40
|
+
tokens = string.split(' ')
|
41
|
+
# p [ :extract, tokens: tokens ]
|
42
|
+
cmd,*args = *tokens
|
43
|
+
# p [ :extract, cmd: cmd, args: args ]
|
44
|
+
new(title: cmd, args: args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Shell
|
49
|
+
attr_reader :pwd
|
50
|
+
|
51
|
+
def initialize(pwd)
|
52
|
+
@pwd = pwd
|
53
|
+
end
|
54
|
+
|
55
|
+
def configuration
|
56
|
+
@config ||= ShellConfig.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def kickstart!
|
60
|
+
loop do
|
61
|
+
print configuration.prompt.call(self)
|
62
|
+
inp = gets.chomp
|
63
|
+
outp = shell_eval(inp)
|
64
|
+
p outp unless outp.nil? || (outp.respond_to?(:empty?) && outp.empty?)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def shell_eval(cmd_str)
|
69
|
+
cmd = ShellCommand.extract(cmd_str)
|
70
|
+
cmd.evaluate(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def change_directory(target)
|
74
|
+
@pwd = target
|
75
|
+
end
|
76
|
+
|
77
|
+
def print_collection(elements)
|
78
|
+
puts
|
79
|
+
elements.each do |element|
|
80
|
+
puts " - " + pastel.blue(" #{element}")
|
81
|
+
end
|
82
|
+
puts
|
83
|
+
end
|
84
|
+
|
85
|
+
def print_warning(message)
|
86
|
+
puts
|
87
|
+
puts pastel.red(message)
|
88
|
+
puts
|
89
|
+
end
|
90
|
+
|
91
|
+
def print_info(message)
|
92
|
+
puts
|
93
|
+
puts pastel.green(message)
|
94
|
+
puts
|
95
|
+
end
|
96
|
+
|
97
|
+
def pastel
|
98
|
+
@pastel ||= Pastel.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.repl!
|
102
|
+
shell = new(Exa['/'])
|
103
|
+
yield shell.configuration
|
104
|
+
shell.kickstart!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Exa
|
2
|
+
class TreeNode
|
3
|
+
attr_reader :name, :parent, :overlays, :links
|
4
|
+
def initialize(name:, value: nil, parent: nil, virtual: false, symbolic: false)
|
5
|
+
p [ :tree_node, name: name ]
|
6
|
+
@name = name
|
7
|
+
@value = value
|
8
|
+
@parent = parent
|
9
|
+
@virtual = virtual
|
10
|
+
@symbolic = symbolic
|
11
|
+
@children = []
|
12
|
+
@overlays = []
|
13
|
+
@links = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
if virtual?
|
18
|
+
constituents.first.value
|
19
|
+
elsif symbolic?
|
20
|
+
dereference_symbolic_link.value
|
21
|
+
else
|
22
|
+
@value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def virtual?
|
27
|
+
@virtual
|
28
|
+
end
|
29
|
+
|
30
|
+
def symbolic?
|
31
|
+
@symbolic
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"<#{@name}>"
|
36
|
+
end
|
37
|
+
|
38
|
+
def path
|
39
|
+
if symbolic?
|
40
|
+
dereference_symbolic_link.path
|
41
|
+
else
|
42
|
+
if @parent
|
43
|
+
slash_name = "/#@name"
|
44
|
+
if @parent.path == '/'
|
45
|
+
slash_name
|
46
|
+
else
|
47
|
+
@parent.path + slash_name
|
48
|
+
end
|
49
|
+
else
|
50
|
+
'/' #@name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update(val)
|
56
|
+
@value = val
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_child(child_name:)
|
61
|
+
child = TreeNode.new(name: child_name, parent: self)
|
62
|
+
@children << child
|
63
|
+
child
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove_child(child_name:)
|
67
|
+
child = @children.detect { |c| c.name == child_name }
|
68
|
+
@children.delete(child)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def unify(overlay:)
|
73
|
+
if overlay.virtual?
|
74
|
+
raise "Won't union mount virtual paths! (Try `link(source: ...)` instead.)"
|
75
|
+
end
|
76
|
+
|
77
|
+
@overlays << overlay
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def link(source:)
|
82
|
+
@links << source
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def children
|
87
|
+
if virtual?
|
88
|
+
virtualize(constituents.flat_map(&:children))
|
89
|
+
else
|
90
|
+
@children + symbolize(symbolic_children) + virtualize(virtual_children)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def recall(path)
|
95
|
+
Visitor.new(self).seek(path)
|
96
|
+
end
|
97
|
+
alias :[] :recall
|
98
|
+
|
99
|
+
def query(path)
|
100
|
+
Visitor.new(self).query(path)
|
101
|
+
end
|
102
|
+
|
103
|
+
def copy(target)
|
104
|
+
Copier.new(self, target).perform!
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete
|
108
|
+
Deleter.new(self).perform!
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
def constituents
|
113
|
+
sources = if parent.virtual?
|
114
|
+
parent.constituents.flat_map(&:children)
|
115
|
+
else
|
116
|
+
parent.overlays.flat_map(&:children)
|
117
|
+
end
|
118
|
+
|
119
|
+
sources.select do |candidate|
|
120
|
+
candidate.name == @name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def virtual_children
|
125
|
+
@overlays.flat_map(&:children)
|
126
|
+
end
|
127
|
+
|
128
|
+
def symbolic_children
|
129
|
+
# this is really a reference
|
130
|
+
@links.flat_map(&:children)
|
131
|
+
end
|
132
|
+
|
133
|
+
def dereference_symbolic_link
|
134
|
+
# okay, we're symbolic... so our parents created us
|
135
|
+
# and have a link
|
136
|
+
parent.links.flat_map(&:children).detect do |linked_child|
|
137
|
+
linked_child.name == @name
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
def symbolize(schildren)
|
143
|
+
schildren.map(&method(:symbolize_one))
|
144
|
+
end
|
145
|
+
|
146
|
+
def symbolize_one(schild)
|
147
|
+
TreeNode.new(name: schild.name, value: schild.value, parent: self, symbolic: true)
|
148
|
+
end
|
149
|
+
|
150
|
+
def virtualize(vchildren)
|
151
|
+
vchildren.map(&method(:virtualize_one))
|
152
|
+
end
|
153
|
+
|
154
|
+
def virtualize_one(vchild)
|
155
|
+
TreeNode.new(name: vchild.name, value: vchild.value, parent: self, virtual: true)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
data/lib/exa/version.rb
ADDED
data/lib/exa/visitor.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Exa
|
2
|
+
class Visitor
|
3
|
+
def initialize(root)
|
4
|
+
@root = root
|
5
|
+
end
|
6
|
+
|
7
|
+
def gather_leaves(branch=@root, depth: 10)
|
8
|
+
return [] if depth < 0
|
9
|
+
if branch.children.any?
|
10
|
+
branch.children.flat_map do |child|
|
11
|
+
gather_leaves(child, depth: depth-1)
|
12
|
+
end.uniq
|
13
|
+
else # we are a leaf!
|
14
|
+
[ branch ]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def gather_branches(branch=@root, depth: 10)
|
19
|
+
return [] if depth < 0
|
20
|
+
if branch.children.any?
|
21
|
+
[ branch ] + branch.children.flat_map do |child|
|
22
|
+
gather_branches(child, depth: depth-1)
|
23
|
+
end.uniq
|
24
|
+
else # we are a leaf!
|
25
|
+
[ ]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def seek(path, create_missing: true)
|
30
|
+
path = '/' + path unless path.start_with?('/')
|
31
|
+
_root, *path_segments = path.split('/')
|
32
|
+
current = @root
|
33
|
+
path_segments.each do |segment|
|
34
|
+
next_child = current.children.detect do |child|
|
35
|
+
child.name == segment
|
36
|
+
end
|
37
|
+
|
38
|
+
current = if next_child
|
39
|
+
next_child
|
40
|
+
else
|
41
|
+
return nil unless create_missing
|
42
|
+
current.create_child(child_name: segment)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
current
|
46
|
+
end
|
47
|
+
|
48
|
+
def query(path)
|
49
|
+
path = '/' + path unless path.start_with?('/')
|
50
|
+
_root, next_segment, *remaining_segments = path.split('/')
|
51
|
+
remaining_path = '/' + remaining_segments.join('/')
|
52
|
+
current = @root
|
53
|
+
return [current] unless next_segment
|
54
|
+
if next_segment == '*'
|
55
|
+
# need to multiplex remaining query across *all* children
|
56
|
+
next_children = current.children
|
57
|
+
if remaining_segments.any?
|
58
|
+
next_children.flat_map do |child|
|
59
|
+
child.query(remaining_path).uniq
|
60
|
+
end
|
61
|
+
else
|
62
|
+
next_children
|
63
|
+
end
|
64
|
+
elsif next_segment == '**'
|
65
|
+
# this is more subtle, and really points to:
|
66
|
+
# do we need to be treating the path query as a regular expression?
|
67
|
+
# and matching against *all* filenames?
|
68
|
+
if remaining_segments.any?
|
69
|
+
gather_branches(current).flat_map do |folder|
|
70
|
+
folder.query(remaining_path).uniq
|
71
|
+
end
|
72
|
+
else
|
73
|
+
gather_leaves(current)
|
74
|
+
end
|
75
|
+
elsif next_segment == '..'
|
76
|
+
# need to back up...
|
77
|
+
parent = current.parent
|
78
|
+
if remaining_segments.any?
|
79
|
+
parent.children.flat_map do |child|
|
80
|
+
child.query(remaining_path).uniq
|
81
|
+
end.uniq
|
82
|
+
else
|
83
|
+
[ parent ]
|
84
|
+
end
|
85
|
+
else
|
86
|
+
# need to find just child matching *this* segment
|
87
|
+
next_child = current.children.detect { |c| c.name == next_segment }
|
88
|
+
if next_child
|
89
|
+
if remaining_segments.any?
|
90
|
+
next_child.query(remaining_path).uniq
|
91
|
+
else
|
92
|
+
[ next_child ]
|
93
|
+
end
|
94
|
+
else
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/spec/exa_spec.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pry'
|
3
|
+
require 'exa'
|
4
|
+
|
5
|
+
describe Exa do
|
6
|
+
before do
|
7
|
+
Exa.clean_slate!
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should store a value' do
|
11
|
+
Exa.remember('x', 5)
|
12
|
+
Exa.remember('y', 7)
|
13
|
+
expect(Exa.recall('x').value).to eq(5)
|
14
|
+
expect(Exa.recall('y').value).to eq(7)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'has shorthand' do
|
18
|
+
Exa['hi'] = 'hello'
|
19
|
+
expect( Exa['hi'].value ).to eq('hello')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should store a structured value' do
|
23
|
+
Exa.remember('/hello/world', 5)
|
24
|
+
expect(Exa.recall('/hello/world').value).to eq(5)
|
25
|
+
|
26
|
+
# shorthand
|
27
|
+
expect(Exa['/hello/world'].value).to eq(5)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should navigate' do
|
31
|
+
Exa.remember("/a/b/c", 'd')
|
32
|
+
expect(Exa["/a/b/c"].value).to eq('d')
|
33
|
+
expect(Exa["/a/b/c"].path).to eq('/a/b/c')
|
34
|
+
expect(Exa["/a"].children).to eq([Exa["/a/b"]])
|
35
|
+
expect(Exa["/a"].parent).to eq(Exa["/"])
|
36
|
+
|
37
|
+
expect(Exa['/'].path).to eq('/')
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'filesystem-like structures' do
|
41
|
+
before do
|
42
|
+
Exa["/usr/joe/minutes/alpha"] = "hello"
|
43
|
+
Exa["/usr/joe/books/ch01/sec01/intro"] = "welcome"
|
44
|
+
Exa["/usr/mal/minutes/beta"] = "world"
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should unify/virtualize' do
|
48
|
+
Exa["/usr/mal/friends"].unify(overlay: Exa["/usr/joe"])
|
49
|
+
|
50
|
+
expect( Exa["/usr/mal/friends/minutes"] ).to be_virtual
|
51
|
+
expect( Exa["/usr/mal/friends/minutes/alpha"] ).to be_virtual
|
52
|
+
expect( Exa["/usr/mal/friends/minutes/alpha"].value ).to eq('hello')
|
53
|
+
expect( Exa["/usr/mal/friends/books/ch01/sec01/intro"].value ).to eq('welcome')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should symlink' do
|
57
|
+
# okay, make a union mount so we have virtual paths...
|
58
|
+
Exa["/usr/joe/friends"].unify(overlay: Exa["/usr/mal"])
|
59
|
+
|
60
|
+
expect( Exa['/usr/joe/friends/minutes'] ).to be_virtual
|
61
|
+
|
62
|
+
# we should be able to *symlink* to virtual paths
|
63
|
+
# which we can't otherwise mount directly (since virtual)
|
64
|
+
Exa['/news'].link(source: Exa['/usr/joe/friends/minutes'])
|
65
|
+
|
66
|
+
expect( Exa['/news/beta'] ).to be_symbolic
|
67
|
+
expect( Exa['/news/beta'].value ).to eq('world')
|
68
|
+
expect( Exa['/news/beta'].path ).to eq('/usr/joe/friends/minutes/beta')
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'path expansion' do
|
72
|
+
it 'should expand root path' do
|
73
|
+
expect( Exa.expand('/') ).to eq([ Exa['/'] ])
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should expand user path without a slash' do
|
77
|
+
expect( Exa.expand('usr')).to eq([ Exa['/usr'] ])
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should expand double stars' do
|
81
|
+
expect( Exa.expand('**/joe') ).to eq([ Exa['/usr/joe'] ])
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should expand single stars' do
|
85
|
+
expect( Exa.expand('/usr/*') ).to eq([ Exa['/usr/joe'], Exa['/usr/mal'] ])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should expand nested single stars' do
|
89
|
+
expect( Exa.expand('/usr/joe/*/*') ).to eq([ Exa['/usr/joe/minutes/alpha'], Exa['/usr/joe/books/ch01'] ])
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should expand double dots' do
|
93
|
+
expect( Exa.expand('/usr/..') ).to eq([ Exa['/'] ])
|
94
|
+
expect( Exa.expand('/usr/joe/..') ).to eq([ Exa['/usr'] ])
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should not find expansions for inexistent paths' do
|
98
|
+
expect( Exa.expand('/does_not_exist') ).to eq([])
|
99
|
+
expect( Exa.expand('/also/does/not/exist') ).to eq([])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should create/nav arbitrary structures' do
|
104
|
+
abel_route = '/universes/ea/regions/blessed_isles/cities/numenor/people/abel'
|
105
|
+
cain_route = '/universes/ea/regions/blessed_isles/cities/numenor/people/cain'
|
106
|
+
Exa[abel_route] = 'hi from abel!'
|
107
|
+
Exa[cain_route] = 'hi from cain!'
|
108
|
+
expect( Exa.expand('/**/people/*') ).to eq([ Exa[abel_route], Exa[cain_route] ])
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should perform some fs-like operations' do
|
112
|
+
Exa['/tmp/hello'] = 'world'
|
113
|
+
|
114
|
+
Exa['/tmp/hello'].copy(Exa['/tmp/there'])
|
115
|
+
Exa['/tmp/hello'].delete
|
116
|
+
|
117
|
+
expect( Exa['/tmp/there'].value ).to eq('world')
|
118
|
+
expect( Exa['/tmp/hello'].value ).to eq(nil)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: exa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joseph Weissman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: codeclimate-test-reporter
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cucumber
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.10.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.10.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubygems-tasks
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.8'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.8'
|
111
|
+
description: recursive in-memory structures
|
112
|
+
email: jweissman1986@gmail.com
|
113
|
+
executables:
|
114
|
+
- exa
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files:
|
117
|
+
- ChangeLog.md
|
118
|
+
- LICENSE.txt
|
119
|
+
- README.md
|
120
|
+
files:
|
121
|
+
- ".document"
|
122
|
+
- ".gitignore"
|
123
|
+
- ".rspec"
|
124
|
+
- ".yardopts"
|
125
|
+
- ChangeLog.md
|
126
|
+
- Gemfile
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.md
|
129
|
+
- Rakefile
|
130
|
+
- bin/exa
|
131
|
+
- exa.gemspec
|
132
|
+
- features/.gitkeep
|
133
|
+
- features/exa.feature
|
134
|
+
- features/step_definitions/.gitkeep
|
135
|
+
- features/step_definitions/exa_steps.rb
|
136
|
+
- gemspec.yml
|
137
|
+
- lib/exa.rb
|
138
|
+
- lib/exa/shell.rb
|
139
|
+
- lib/exa/tree_node.rb
|
140
|
+
- lib/exa/version.rb
|
141
|
+
- lib/exa/visitor.rb
|
142
|
+
- spec/exa_spec.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
homepage: https://rubygems.org/gems/exa
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
metadata: {}
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 2.5.1
|
165
|
+
signing_key:
|
166
|
+
specification_version: 4
|
167
|
+
summary: recursive in-memory algos and data structures
|
168
|
+
test_files: []
|