Last active
October 13, 2017 15:46
-
-
Save mklbtz/416d50223566b3fa411cc264502855c0 to your computer and use it in GitHub Desktop.
A DSL for safely building file paths
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AbsoluteDirectory < AbstractPath | |
def self.root | |
AbsoluteDirectory.new(nodes: []) | |
end | |
def initialize(nodes:) | |
raise "Last node cannot be a file, got (#{nodes.last.class})" if nodes.last&.file? | |
if nodes.first&.root? | |
super(nodes: nodes) | |
elsif nodes.first&.current? | |
super(nodes: nodes.drop(1).unshift(RootNode.new)) | |
else | |
super(nodes: nodes.unshift(RootNode.new)) | |
end | |
end | |
def initialize_named(name) | |
@nodes = [RootNode.new, DirectoryNode.new(name)] | |
end | |
def initialize_converting(path) | |
initialize(nodes: path.nodes) | |
end | |
def absolute? | |
true | |
end | |
def file? | |
false | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AbsoluteFile < AbstractPath | |
def initialize(nodes:) | |
raise "Last node must be a file, got (#{nodes.last.class})" unless nodes.empty? || nodes.last.file? | |
if nodes.first&.root? | |
super(nodes: nodes) | |
elsif nodes.first&.current? | |
super(nodes: nodes.drop(1).unshift(RootNode.new)) | |
else | |
super(nodes: nodes.unshift(RootNode.new)) | |
end | |
end | |
def initialize_named(name) | |
@nodes = [RootNode.new, FileNode.new(name)] | |
end | |
def initialize_converting(path) | |
initialize(nodes: path.nodes) | |
end | |
def absolute? | |
true | |
end | |
def file? | |
true | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AbstractPath | |
attr_reader :nodes | |
def initialize(nodes:) | |
@nodes = nodes | |
end | |
def self.named(name) | |
obj = allocate | |
obj.initialize_named(name) | |
obj | |
end | |
def self.converting(path) | |
obj = allocate | |
obj.initialize_converting(path) | |
obj | |
end | |
def relative? | |
!absolute? | |
end | |
def directory? | |
!file? | |
end | |
def to_relative | |
return self if relative? | |
if directory? | |
RelativeDirectory.converting(self) | |
elsif file? | |
RelativeFile.converting(self) | |
else | |
raise "Impossible! How am I not a file or directory?" | |
end | |
end | |
def to_absolute | |
return self if absolute? | |
if directory? | |
AbsoluteDirectory.converting(self) | |
elsif file? | |
AbsoluteFile.converting(self) | |
else | |
raise "Impossible! How am I not a file or directory?" | |
end | |
end | |
def + (other) | |
raise "Cannot append an absolute path (#{self} + #{other})" if other.absolute? | |
raise "Cannot append to a file path (#{self} + #{other})" if file? | |
combined = nodes + other.nodes.drop(1) | |
if absolute? && other.directory? | |
AbsoluteDirectory.new(nodes: combined) | |
elsif absolute? && other.file? | |
AbsoluteFile.new(nodes: combined) | |
elsif relative? && other.directory? | |
RelativeDirectory.new(nodes: combined) | |
elsif relative? && other.file? | |
RelativeFile.new(nodes: combined) | |
end | |
end | |
def to_s | |
nodes.join('/') + (directory? ? '/' : '') | |
end | |
def inspect | |
nodes.inspect | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def current | |
RelativeDirectory.current | |
end | |
def root(name = nil) | |
if name | |
AbsoluteDirectory.root + dir(name) | |
else | |
AbsoluteDirectory.root | |
end | |
end | |
def dir(name) | |
RelativeDirectory.named(name) | |
end | |
def file(name) | |
RelativeFile.named(name) | |
end | |
bin = root("usr") + dir("local") + dir("bin") | |
ruby = bin + file("ruby") | |
# => ["/", "usr", "local", "bin", "ruby"] | |
ruby.to_s | |
# => "/usr/local/bin/ruby" | |
file("ruby") + bin | |
# ~> RuntimeError | |
# ~> Cannot append an absolute path (./ruby + /usr/local/bin/) | |
file("ruby") + dir("bad") | |
# ~> RuntimeError | |
# ~> Cannot append to a file path (./ruby + ./bad/) | |
root + current + current + current | |
# => ["/"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class PathNode | |
def root? | |
false | |
end | |
def current? | |
false | |
end | |
def directory? | |
false | |
end | |
def file? | |
false | |
end | |
private | |
def escape(name) | |
name # TODO: something smart like escaping spaces and slashes | |
end | |
end | |
class RootNode < PathNode | |
def root? | |
true | |
end | |
def to_s | |
'' # when joined, this will look like '/' | |
end | |
def inspect | |
'/'.inspect | |
end | |
end | |
class CurrentNode < PathNode | |
def current? | |
true | |
end | |
def to_s | |
'.' # when joined, this will look like './' | |
end | |
def inspect | |
to_s.inspect | |
end | |
end | |
class DirectoryNode < PathNode | |
attr_reader :name | |
def initialize(name) | |
@name = escape(name) | |
end | |
def directory? | |
true | |
end | |
alias to_s name | |
def inspect | |
name.inspect | |
end | |
end | |
class FileNode < PathNode | |
attr_reader :name | |
def initialize(name) | |
@name = escape(name) | |
end | |
def file? | |
true | |
end | |
alias to_s name | |
def inspect | |
name.inspect | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RelativeDirectory < AbstractPath | |
def self.current | |
RelativeDirectory.new(nodes: []) | |
end | |
def initialize(nodes:) | |
raise "Last node cannot be a file, got (#{nodes.last.class})" if nodes.last&.file? | |
if nodes.first&.root? | |
super(nodes: nodes.drop(1).unshift(CurrentNode.new)) | |
elsif nodes.first&.current? | |
super(nodes: nodes) | |
else | |
super(nodes: nodes.unshift(CurrentNode.new)) | |
end | |
end | |
def initialize_named(name) | |
@nodes = [CurrentNode.new, DirectoryNode.new(name)] | |
end | |
def initialize_converting(path) | |
initialize(nodes: path.nodes) | |
end | |
def absolute? | |
false | |
end | |
def file? | |
false | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RelativeFile < AbstractPath | |
def initialize(nodes:) | |
raise "Last node must be a file, got (#{nodes.last.class})" unless nodes.empty? || nodes.last.file? | |
if nodes.first&.root? | |
super(nodes: nodes.drop(1).unshift(CurrentNode.new)) | |
elsif nodes.first&.current? | |
super(nodes: nodes) | |
else | |
super(nodes: nodes.unshift(CurrentNode.new)) | |
end | |
end | |
def initialize_named(name) | |
@nodes = [CurrentNode.new, FileNode.new(name)] | |
end | |
def initialize_converting(path) | |
initialize(nodes: path.nodes) | |
end | |
def absolute? | |
false | |
end | |
def file? | |
true | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment