Oyatul
NAME
Oyatul - Abstract representation of filesystem layout
SYNOPSIS
This runs the tests identified by 'purpose' test which can be in any location in the layout with the library directory identified by the purpose 'lib' :
use Oyatul;
my $description = q:to/LAY/;
{
"type" : "layout",
"children" : [
{
"name" : "t",
"purpose" : "tests",
"type" : "directory",
"children" : [
{
"type" : "file",
"purpose" : "test",
"template" : true
}
]
},
{
"type" : "directory",
"purpose" : "lib",
"name" : "lib",
"children" : []
}
]
}
LAY
# the :real adverb causes instance nodes to be inserted
# for any templates if they exist.
my $layout = Oyatul::Layout.from-json($description, root => $*CWD.Str, :real);
# get the directory that stands in for 'lib'
my $lib = $layout.nodes-for-purpose('lib').first.path;
# get all the instances for 'test' excluding the template
for $layout.nodes-for-purpose('test', :real) -> $test {
run($*EXECUTABLE, '-I', $lib, $test.path);
}
DESCRIPTION
This provides a method of describing a filesystem layout in an abstract manner.
It can be used in the deployment of applications which might need the creation of a directory tree for data or configuration, or for applications which may need to locate files and directory that it needs but can allow the user to define their own .
The file layout descriptions can be stored as JSON or they can be built programmatically (thus allowing other forms of storage.)
The description can define directories and files in an aribitrary tree structure, each can optionally define a 'purpose' which can be used to locate a node irrespective of its location in the tree and name, a node object can also be given a role with the 'does' key which can give the node additional behaviours (e.g. create a file of a specific format, create an object based on a file or directory etc.) Template nodes can be defined which can stand in for real files or directories which can be discovered at run-time.
This is based on a design that I used in a large application that relied
heavily on file storage for its data, but is somewhat more simplified
and abstracted as well as preferring JSON over the original XML for the
storage of the layout description. The features are designed to allow
Sofa to load a CouchDB design
document from an arbitrary (possibly user defined) file hierarchy unlike
couchapp
which requires a fixed directory structure. However hopefully
it will be useful in other applications.
class Oyatul::Layout
class Oyatul::Layout does Oyatul::Parent
This is the top level description of the layout.
method new
method new(Oyatul::Layout:U: -> Oyatul::Layout);
This is the constructor for the class, typically one would prefer one
of the generate
or from-json
, but this may be used if one is
creating a layout programmatically from some other form of configuration.
method generate
method generate (Oyatul::Layout: Str :$root = '.' --> Oyatul::Layout)
method generate (Oyatul::Layout: IO::Path:D :$root!)
This will create a new Oyatul::Layout based on the directory structure
found in $root
(which defaults to the current directory,) it will by
default skip any files or directories who's name begins with '.'
method from-json
method from-json (Oyatul::Layout:U: IO::Path :$path!, |c is raw --> Oyatul::Layout)
method from-json (Oyatul::Layout:U: Str :$path!, |c is raw --> Oyatul::Layout)
method from-json (Oyatul::Layout:U: Str $json, Str :$root = '.', Bool :$real --> Oyatul::Layout)
This returns a new Oyatul::Layout based on the JSON that can be passed
as a string, or (with path
) as the path to a file containing JSON.
If root
is supplied this will be the path where the layout is anchored
(this defaults to the current directory.) If the :real
adverb is
supplied any templates found in the layout will have File or Directory
instances created for the files or directories that are in the position
of the template.
The format of the JSON is described below.
method from-hash
method from-hash (Oyatul::Layout: %h, :$root)
This returns a new Oyatul::Layout based on a Hash containing data of the same format as the JSON.
method to-json
method to-json (Oyatul::Layout:)
This returns a JSON string that describes the layout, it can be
round-tripped through from-json
, but is primarily intended to get
a layout discovered by generate
which may be edited to suit the
application.
method path-parts
method path-parts (Oyatul::Layout:)
This returns a list containing the value of root
, it will be used to
create the paths of the child nodes.
method create
method create (Oyatul::Layout: Str :$root --> Bool)
This causes all the 'real' (i.e. non-template) nodes in the layout to
be created starting at the top-level by calling the create
methods
in turn. It returns a Bool to indicate whether all the creations were
successfull.
method IO
method IO (Oyatul::Layout: --> IO::Path)
This returns an IO::Path object for the root
of the layout.
class Oyatul::File
class Oyatul::File does Oyatul::Node
This represents a 'file' leaf-node in the layout.
method to-hash
method to-hash (Oyatul::File:D:)
Returns a representation of the File object as a Hash, which will be used by its parent to get its Hash and eventually that for the whole layout.
method from-hash
method from-hash (Oyatul::File: %h, Oyatul::Parent:D :$parent)
Creates a new Oyatul::File object from the supplied Hash, if it is
to be inserted into a layout then parent
should be supplied (this
will be done by a Oyatul::Parent when it is creating its children
from a hash using this method.)
method create
method create (Oyatul::File: --> Bool)
This will attempt to create an empty file with the name of this File
in the appropriate location, returning a Bool to indicate whether this
was successful, if something other than an empty file is to be created
then this can be over-ridden from a role specified in the does
key
in the layout.
method delete
method delete (Oyatul::File: --> Bool)
This will attempt to delete the physical file that this File object represents in the layout, returning a Bool indicating success.
method accepts-path
method accepts-path (Oyatul::File: IO::Path:D $path --> Bool)
This returns a Bool to indicate whether the supplied IO::Path
represents a file, it will be called by realise-template
with
the IO::Path object representing a directory entry to determine
whether it is suitable match for the template. If some more
detailed check is required this can be over-ridden in a role
specified by the does
key for the template.
class Oyatul::Directory
class Oyatul::Directory does Oyatul::Node does Oyatul::Parent
This represents a directory node in the layout, it can have file or directory children.
method generate
method generate (Oyatul::Directory: IO::Path:D :$root!, Oyatul::Parent :$parent!)
This will return a new Oyatul::Directory with the name of the
basename of the supplied root
and with all the child nodes populated
iteratively from those found in the filesystem, parent
should be
either a Oyatul::Layout or another Oyatul::Directory. This will
typically be called via the Oyatul::Layout generate
method.
method from-hash
method from-hash (Oyatul::Directory:U: %h, Oyatul::Parent:D :$parent)
This creates a new Oyatul::Directory based on the supplied Hash. If
it is to be inserted into a layout then parent
should be supplied, this
will be done when it is being called via the Oyatul::Layout from-hash
.
method create
method create (Oyatul::Directory: --> Bool)
This will attempt to create the filesystem structure this directory
represents by creating itself (with mkdir
) and iteratively calling
create
on all of the children. It returns a Bool indicating
whether all creation was successful.
method accepts-path
method accepts-path (Oyatul::Directory: IO::Path:D $path --> Bool)
This returns a Bool to indicate whether the supplied IO::Path
represents a directory, it will be called by realise-template
with
the IO::Path object representing a directory entry to determine
whether it is suitable match for the template. If some more
detailed check is required this can be over-ridden in a role
specified by the does
key for the template.
role Oyatul::Parent
This is a role for classes that can contain child objects, typically Oyatul::Layout and Oyatul::Directory.
attribute @.children
has Oyatul::Node @.children
This is the collection of the child nodes of this object.
method all-children
method all-children (Oyatul::Parent: Bool :$real)
This returns a list of all the child objects of the object. If this is a Layout object then it will be *all* the nodesi.
method template-for-purpose
method template-for-purpose (Oyatul::Parent: Str $purpose --> Oyatul::Template)
This returns the Oyatul::Template that has the specified purpose if one exists.
If more than one template exists for the same purpose then only the first found will be returned, this implies that having more than one should be avoided.
method nodes-for-purpose
method nodes-for-purpose (Oyatul::Parent: Str $purpose, Bool :$real)
This returns a list of all the Oyatul::Node objects that have the
specified purpose. If the real
adverb is supplied only the non-template
nodes are returned.
method gather-children
method gather-children (Oyatul::Parent: IO::Path:D $root)
This is used by the generate
method to discover the nodes in
the directory tree, returning a list of Oyatul::Node objects
which will be created by calling generate
on them for each
file or directory in $root
method child-by-name
method child-by-name (Oyatul::Parent: Str $name --> Oyatul::Node)
Returns the Oyatul::Node (a Oyatul::File or Oyatul::Directory) that has the specified name.
method delete
method delete (Oyatul::Parent: --> Bool)
This will attempt to delete the entire tree starting at this node, by
calling delete
on each node depth-first. It returns a Bool indicating
whether all the deletions were successful. Care should be taken when
using this on an existing structure which may be shared with another
application as whilst it will only attempt to delete those nodes described
by the layout, it will attempt to delete the root
if this is a Layout
object.
method realise-templates
method realise-templates (Oyatul::Parent:)
This will attempt to create 'real' Oyatul::Node objects for all nodes that are found in the place of a template in the layout, that is if the template is a file and a file is found in the enclosing directory then a Oyatul::File with the name of the file will be created and inserted into the layout instance.
This returns a list of all the instances that were created.
role Oyatul::Template
role Oyatul::Template[Mu:U $real-type]
This role is applied to a node in the layout which has a True value
for the template
key, it is a placeholder for any number of
named real nodes that may not be known until an instance of the
layout is applied to the filesystem.
The role is parameterised with the real (original type) of the
node (i.e. Oyatul::File or Oyatul::Directory) which will
be used to instantiate the 'real' object with make-real
.
method create
method create ($?CLASS:)
A Template cannot actually exist so this is a stub that returns true without doing anything.
method delete
method delete ($?CLASS:)
A Template cannot actually exist so this is a stub that returns true without doing anything.
method make-real
method make-real ($?CLASS: Str $name)
This is called on the template object by gather-instances
with
the name of each matching node that is found. It returns a 'real'
Oyatul::Node (that is one without the Template role,) that has
been inserted into the layout (i.e added to the children
of
the parent of the Template and parent
populated appropriately.
method gather-instances
method gather-instances ($?CLASS:)
This will be called by realise-templates
for each template in the
tree. For each filesystem node in the same location as the template
for which accepts-path returns true, make-real
will be called with
the name of the node. This returns a list of the 'real' instances that
were created.
method is-template
method is-template ($?CLASS:)
This returns True for any object that does this role.
role Oyatul::Node
This is role for items in the Layout, (i.e. File, Directory)
attribute name
has Str $.name;
This is the basename of the filesystem node in layout, it is required for all objects that are not templates.
attribute parent
has Oyatul::Parent $.parent;
This is the parent object of the node. This will typically be populated by the parent object itself when the object is being added to the layout.
attribute purpose
has Str $.purpose
This is the 'purpose' as defined by the same named key in the
layout description. It is an arbitrary string that will be
matched by the nodes-for-purpose
method of Oyatul::Parent.
method IO
method IO (Oyatul::Node: --> IO::Path)
Returns the IO::Path object representing the path
in the layout.
method path
method path (Oyatul::Node: --> Str)
Returns the full path of the node in the layout, it will be anchored at
the root
of the layout instance.
method path-parts
method path-parts (Oyatul::Node:)
This returns a list of the individual parts of the path of the node, the
first element will be the root
of the layout.
method is-template
method is-template (Oyatul::Node: --> Bool)
This will return a Bool indicating if this Node
is infact a Template
(that is it does the Oyatul::Template role,)
method create
method create ($?CLASS: --> Bool)
This is an "abstract" method that must be defined by the composing class.
method delete
method delete ($?CLASS: --> Bool)
This is an "abstract" method that must be defined by the composing class.
method accepts-path
method accepts-path ($?CLASS: IO::Path:D $ --> Bool)
This is an "abstract" method that must be defined by the composing class.
DESCRIPTION FORMAT
The layout description will typically be stored as a JSON formatted file, but could be stored in any format that can be de-serialised to a similarly structured Hash. Alternatively the description could be stored in any medium and the layout objects created individually based on the data.
The top level item should be a Hash (or JSON Object,) with the key type
with the value 'layout', and children
which will be an Array of
Objects describing the child nodes.
NODE KEYS
name
type
This is mandatory and must be either file
or directory
(except at
the top level where it must be layout
.) If any other value is found
in traversing the description an exception will be thrown.
If the type is file
it will result in a Oyatul::File, if directory
then Oyatul::Directory.
purpose
This is an optional key, that can be an arbitrary string, that will be
matched by nodes-for-purpose
.
children
This is mandatory for nodes of type
'directory' or 'layout' type and
must be an Array of node objects (or an empty Array,) that represent the
child objects.
does
This is optional and if present will be the short name of a role that will be required (if it isn't already loaded,) and mixed in to the Oyatul::Node that is instantiated from the description. If it cannot be loaded or the name doesn't specify a composable type then an exception will be thrown.
The role can supply behaviour that is specific to the application
or over-ride the methods of Oyatul::Node (e.g. a create
that
will populate a data file in a certain way.)
template
This is an optional boolean that indicates whether the node is a template or not, if it is true then the role Oyatul::Template will be applied to the node object when it is being added to the layout.