Skip to content

Instantly share code, notes, and snippets.

@mklbtz
Last active October 13, 2017 15:46
Show Gist options
  • Save mklbtz/416d50223566b3fa411cc264502855c0 to your computer and use it in GitHub Desktop.
Save mklbtz/416d50223566b3fa411cc264502855c0 to your computer and use it in GitHub Desktop.
A DSL for safely building file paths
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
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
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
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
# => ["/"]
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
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
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