Navigate Perl6 containers and perform tasks
Perl6
Latest commit 1be8aa8 Mar 29, 2017 @atweiden make tests green
adapt to new rakudo changes

README.md

Crane

Navigate Perl6 containers and perform tasks.

Synopsis

use Crane;

my %h0;
my %h1 = Crane.add(%h0, :path['a'], :value({:b({:c<here>})}));
my %h2 = Crane.add(%h1, :path(qw<a b d>), :value([]));
my %h3 = Crane.add(%h2, :path(|qw<a b d>, 0), :value<diamond>);
my %h4 = Crane.replace(%h3, :path(|qw<a b d>, *-1), :value<dangerous>);
my %h5 = Crane.remove(%h4, :path(qw<a b c>));
my %h6 = Crane.move(%h5, :from(qw<a b d>), :path['d']);
my %h7 = Crane.copy(%h6, :from(qw<a b>), :path['b']);
my %h8 = Crane.remove(%h7, :path(qw<a b>));
my %h9 = Crane.replace(%h8, :path['a'], :value(['alligators']));
my %h10 = Crane.replace(%h9, :path['b'], :value(['be']));

say Crane.list(%h10).perl;
(
    {:path(["a", 0]), :value("alligators")},
    {:path(["b", 0]), :value("be")},
    {:path(["d", 0]), :value("dangerous")}
)

Description

Crane aims to be for Perl6 containers what JSON Pointer and JSON Patch are for JSON.

Features

  • add, remove, replace, move, copy, test operations
  • get/set
  • diff/patch
  • list the contents of a nested data structure in accessible format

Exported Subroutines

at($container,*@path)

Navigates to and returns container is rw.

arguments:

  • $container: Container, required — the target container
  • *@path: Path, optional — a list of steps for navigating container

returns:

  • Container at path (is rw)

example:

my %inxi = :info({
    :memory([1564.9, 32140.1]),
    :processes(244),
    :uptime<3:16>
});

at(%inxi, 'info')<uptime>:delete;
at(%inxi, qw<info memory>)[0] = 31868.0;

say %inxi.perl; # :info({ :memory(31868.0, 32140.1), :processes(244) })

in($container,*@path)

in works like at, except in will create the structure of nonexisting containers based on @path input instead of aborting the operation, e.g.

at(my %h, qw<a b c>) = 5 # ✗ Crane error: associative key does not exist
in(my %i, qw<a b c>) = 5
say %i.perl;
{
    :a({
        :b({
            :c(5)
        })
    })
}

If @path contains keys that lead to a nonexisting container, the default behavior is to create a Positional container there if the key is an Int >= 0 or a WhateverCode. Otherwise in creates an Associative container there, e.g.

my %h;
in(%h, qw<a b>, 0, *-1) = 'here';
say %h.perl;
{
    :a({
        :b([
            ["here"]
        ])
    })
}

If @path contains keys that lead to an existing Associative container, it will attempt to index the existing Associative container with the key regardless of the key's type (be it Int or WhateverCode), e.g.

my @a = [ :i({:want({:my<MTV>})}) ];
in(@a, 0, 'i', 'want', 2) = 'always';
say @a.perl;
[
    :i({
        :want({
            "2" => "always",
            :my("MTV")
        })
    })
]

my @b = [ :i({:want(['my', 'MTV'])}) ];
in(@b, 0, 'i', 'want', 2) = 'always';
say @b.perl;
[
    :i({
        :want(["my", "MTV", "always"])
    })
]

arguments:

  • $container: Container, required — the target container
  • *@path: Path, optional — a list of steps for navigating container

returns:

  • Container at path (is rw)

example:

my %archversion = :bamboo({ :up<0.0.1>, :aur<0.0.2> });
in(%archversion, qw<fzf up>) = '0.11.3';
in(%archversion, qw<fzf aur>) = '0.11.3';
say %archversion.perl;
{
    :bamboo({ :aur<0.0.2>, :up<0.0.1> }),
    :fzf({ :aur<0.11.3>, :up<0.11.3> })
}

Methods

All example code assumes %data has this structure:

my %data =
    :legumes([
        {
            :instock(4),
            :name("pinto beans"),
            :unit("lbs")
        },
        {
            :instock(21),
            :name("lima beans"),
            :unit("lbs")
        },
        {
            :instock(13),
            :name("black eyed peas"),
            :unit("lbs")
        },
        {
            :instock(8),
            :name("split peas"),
            :unit("lbs")
        }
    ]);

.exists($container,:@path!,:$k,:$v)

Determines whether a key exists in the container at the specified path. Works similar to the p6 Hash :exists subscript adverb. Pass the :v flag to determine whether a defined value is paired to the key at the specified path.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$k: Bool, optional, defaults to True — indicates whether to check for an existing key at path
  • :$v: Bool, optional — indicates whether to check for a defined value at path

returns:

  • True if exists, otherwise False

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container. Passing :v flag tests if $container.defined. Default behavior is to raise an error as key operations aren't permitted on root containers.

.get($container,:@path!,:$k,:$v,:$p)

Gets the value from container at the specified path. The default behavior is to raise an error if path is nonexistent.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$k: Bool, optional — only return the key at path
  • :$v: Bool, optional, defaults to True — only return the value at path
  • :$p: Bool, optional — return the key-value pair at path

returns:

  • the dereferenced key, value or key-value pair

example:

my $value = Crane.get(%data, :path('legumes', 1));
say $value.perl; # { :instock(21), :name("lima beans"), :unit("lbs") }

my $value-k = Crane.get(%data, :path('legumes', 1), :k);
say $value-k.perl; # 1

my $value-p = Crane.get(%data, :path('legumes', 1), :p);
say $value-p.perl; # 1 => { :instock(21), :name("lima beans"), :unit("lbs") }

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

  • if :v flag passed (the default): return $container
  • if :k flag passed: raise error "Sorry, not possible to request key operations on the container root"
  • if :p flag passed: raise error "Sorry, not possible to request key operations on the container root"

.set($container,:@path!,:$value!)

Sets the value at the specified path in the container. The default behavior is to create nonexistent paths (similar to mkdir -p).

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$value!: Any, required — the value to be set at the specified path

returns:

  • Modified container

example:

my %p;
Crane.set(%p, :path(qw<peter piper>), :value<man>);
Crane.set(%p, :path(qw<peter pan>), :value<boy>);
Crane.set(%p, :path(qw<peter pickle>), :value<dunno>);
say %p.perl; # { :peter({ :pan("boy"), :pickle("dunno"), :piper("man") }) }

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

my $a = (1, 2, 3);
Crane.set($a, :path(), :value<foo>);
say $a; # foo

.add($container,:@path!,:$value!,:$in-place)

Adds a value to the container. If :@path points to an existing item in the container, that item's value is replaced.

In the case of a Positional type, the value is inserted before the given index. Use the relative accessor (*-0) instead of an index (Int) for appending to the end of a Positional.

Because this operation is designed to add to existing Associative types, its target location will often not exist. However, an Associative type or a Positional type containing it does need to exist, and it remains an error for that not to be the case. For example, a .add operation with a target location of <a b> starting with this Hash:

{ :a({ :foo(1) }) }

is not an error, because "a" exists, and "b" will be added to its value. It is an error in this Hash:

{ :q({ :bar(2) }) }

because "a" does not exist.

Think of the .add operation as behaving similarly to mkdir, not mkdir -p. For example, you cannot do (in shell):

$ ls # empty directory
$ mkdir a/b/c
mkdir: cannot create directory ‘a/b/c’: No such file or directory

Without the -p flag, you'd have to do:

$ ls # empty directory
$ mkdir a
$ mkdir a/b
$ mkdir a/b/c

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$value!: Any, required — the value to be added/inserted at the specified path
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %legume = :name<carrots>, :unit<lbs>, :instock(3);
my %data-new = Crane.add(%data, :path('legumes', 0), :value(%legume));

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

my @a;
my @b = Crane.add(@a, :path([]), :value<foo>);
say @a.perl; # []
say @b.perl; # ["foo"]

.remove($container,:@path!,:$in-place)

Removes the pair at path from Associative types, similar to the p6 Hash :delete subscript adverb. Splices elements out from Positional types.

The default behavior is to raise an error if the target location is nonexistent.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %h = :example<hello>;
my %h2 = Crane.remove(%h, :path(['example']));
say %h.perl; # { :example<hello> }
say %h2.perl; # {}

This:

%h<a><b>:delete;

is equivalent to this:

Crane.remove(%h, :path(qw<a b>));

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

my $a = [1, 2, 3];
my $b = Crane.remove($a, :path([])); # equivalent to `$a = Empty`
say $a.perl; [1, 2, 3]
say $b; # (Any)

.replace($container,:@path!,:$value!,:$in-place)

Replaces a value. This operation is functionally identical to a .remove operation for a value, followed immediately by a .add operation at the same location with the replacement value.

The default behavior is to raise an error if the target location is nonexistent.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$value!: Any, required — the value to be set at the specified path
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %legume = :name("green beans"), :unit<lbs>, :instock(3);
my %data-new = Crane.replace(%data, :path('legumes', 0), :value(%legume));

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

my %a = :a<aaa>, :b<bbb>, :c<ccc>;
my %b = Crane.replace(%a, :path([]), :value({ :vm<moar> }));
say %a.perl; # { :a<aaa>, :b<bbb>, :c<ccc> }
say %b.perl; # { :vm<moar> }

.move($container,:@from!,:@path!,:$in-place)

Moves the source value identified by @from in container to destination location specified by @path. This operation is functionally identical to a .remove operation on the @from location, followed immediately by a .add operation at the @path location with the value that was just removed.

The default behavior is to raise an error if the source is nonexistent.

The default behavior is to raise an error if the @from location is a proper prefix of the @path location; i.e., a location cannot be moved into one of its children.

arguments:

  • $container: Container, required — the target container
  • :@from!: Path, required — a list of steps to the source
  • :@path!: Path, required — a list of steps to the destination
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

What about operating on the root of the container?

Pass an empty list as @from or @path to operate on the root of the container.

.copy($container,:@from!,:@path!,:$in-place)

Copies the source value identified by @from in container to destination container at location specified by @path. This operation is functionally identical to a .add operation at the @path location using the value specified in the @from. As with .move, a location cannot be copied into one of its children.

The default behavior is to raise an error if the source at @from is nonexistent.

arguments:

  • $container: Container, required — the target container
  • :@from!: Path, required — a list of steps to the source
  • :@path!: Path, required — a list of steps to the destination
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %h = :example<hello>;
my %h2 = Crane.copy(%h, :from(['example']), :path(['sample']));
say %h.perl; # { :example("hello") }
say %h2.perl; # { :example("hello"), :sample("hello") }

What about operating on the root of the container?

Pass an empty list as @from or @path to operate on the root of the container. Has similar rules / considerations to .move.

.test($container,:@path!,:$value!)

Tests that the specified value is set at the target location in the document.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps for navigating container
  • :$value!: Any, required — the value expected at the specified path

returns:

  • True if expected value exists at @path, otherwise False

example:

say so Crane.test(%data, :path('legumes', 0, 'name'), :value("pinto beans")); # True

What about operating on the root of the container?

Pass an empty list as @path to operate on the root of the container.

.list($container,:@path)

Lists all of the paths available in $container.

arguments:

  • $container : Container, required — the target container
  • :@path: Path, optional — a list of steps for navigating container

returns:

  • List of path-value pairs

example:

Listing a Hash:

say Crane.list(%data);
(
    {
        :path(["legumes", 0, "instock"]),
        :value(4)
    },
    {
        :path(["legumes", 0, "name"]),
        :value("pinto beans")
    },
    {
        :path(["legumes", 0, "unit"]),
        :value("lbs")
    },
    {
        :path(["legumes", 1, "instock"]),
        :value(21)
    },
    {
        :path(["legumes", 1, "name"]),
        :value("lima beans")
    },
    {
        :path(["legumes", 1, "unit"]),
        :value("lbs")
    },
    {
        :path(["legumes", 2, "instock"]),
        :value(13)
    },
    {
        :path(["legumes", 2, "name"]),
        :value("black eyed peas")
    },
    {
        :path(["legumes", 2, "unit"]),
        :value("lbs")
    },
    {
        :path(["legumes", 3, "instock"]),
        :value(8)
    },
    {
        :path(["legumes", 3, "name"]),
        :value("split peas")
    },
    {
        :path(["legumes", 3, "unit"]),
        :value("lbs")
    }
)

Listing a List:

my $a = qw<zero one two>;
say Crane.list($a);
(
    {
        :path([0]),
        :value("zero")
    },
    {
        :path([1]),
        :value("one")
    },
    {
        :path([2]),
        :value("two")
    }
)

.flatten($container,:@path)

Flattens a container into a single-level Hash of path-value pairs.

arguments:

  • $container : Container, required — the target container
  • :@path: Path, optional — a list of steps for navigating container

returns:

  • a flattened Hash of path-value pairs.

example:

say Crane.flatten(%data);
{
    ("legumes", 0, "instock") => 4,
    ("legumes", 0, "name")    => "pinto beans",
    ("legumes", 0, "unit")    => "lbs",
    ("legumes", 1, "instock") => 21,
    ("legumes", 1, "name")    => "lima beans",
    ("legumes", 1, "unit")    => "lbs",
    ("legumes", 2, "instock") => 13,
    ("legumes", 2, "name")    => "black eyed peas",
    ("legumes", 2, "unit")    => "lbs",
    ("legumes", 3, "instock") => 8,
    ("legumes", 3, "name")    => "split peas",
    ("legumes", 3, "unit")    => "lbs"
}

.transform($container,:@path!,:&with!,:$in-place)

Functionally identical to a replace operation with the replacement value being the return value of with applied to the value of $container at @path.

The Callable passed in with must take one (optional) positional argument and return a value.

arguments:

  • $container: Container, required — the target container
  • :@path!: Path, required — a list of steps to the destination
  • :&with!: Callable, required — instructions for changing the value at @path
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %market =
    :foods({
        :fruits([qw<blueberries marionberries>]),
        :veggies([qw<collards onions>])
    });

my @first-fruit = |qw<foods fruits>, 0;
my @second-veggie = |qw<foods veggies>, 1;

my &oh-yeah = -> $s { $s ~ '!' };

Crane.transform(%market, :path(@first-fruit), :with(&oh-yeah), :in-place);
say so Crane.get(%market, :path(@first-fruit)) eq 'blueberries!'; # True

Crane.transform(%market, :path(@second-veggie), :with(&oh-yeah), :in-place);
say so Crane.get(%market, :path(@second-veggie)) eq 'onions!'; # True

.patch($container,@patch,:$in-place)

Apply @patch, a list of specially formatted Hashes representing individual 6902 operations implemented by Crane, to $container.

add

{ :op("add"), :path(qw<path to target>), :value("Value") }

remove

{ :op("remove"), :path(qw<path to target>) }

replace

{ :op("replace"), :path(qw<path to target>), :value("Value") }

move

{ :op("move"), :from(qw<path to source>), :path(qw<path to target>) }

copy

{ :op("copy"), :from(qw<path to source>), :path(qw<path to target>) }

test

{ :op("test"), :path(qw<path to target>), :value("Is value") }

If an operation is not successful, the default behavior is to raise an exception. If a test operation as part of the @patch returns False, the default behavior is to raise an exception.

arguments:

  • $container: Container, required — the target container
  • @patch: Patch, required — a list of 6902 instructions to apply
  • :$in-place: Bool, optional, defaults to False — whether to modify $container in-place

returns:

  • Container (original container is unmodified unless :in-place flag is passed)

example:

my %h;
my @patch =
    { :op<add>, :path['a'], :value({:b({:c<here>})}) },
    { :op<add>, :path(qw<a b d>), :value([]) },
    { :op<add>, :path(|qw<a b d>, 0), :value<diamond> },
    { :op<replace>, :path(|qw<a b d>, *-1), :value<dangerous> },
    { :op<remove>, :path(qw<a b c>) },
    { :op<move>, :from(qw<a b d>), :path['d'] },
    { :op<copy>, :from(qw<a b>), :path['b'] },
    { :op<remove>, :path(qw<a b>) },
    { :op<replace>, :path['a'], :value(['alligators']) },
    { :op<replace>, :path['b'], :value(['be']) };
my %i = Crane.patch(%h, @patch);

Exception: 42 !== 'C'

my %h = :a({:b({:c})});
my @patch =
    { :op<replace>, :path(qw<a b c>), :value(42) },
    { :op<test>, :path(qw<a b c>), :value<C> };
my %i = Crane.patch(%h, @patch); # ✗ Crane error: patch operation failed, test failed

Classes

Crane Class

Encapsulates container related operations for a specified path.

attributes:

  • .path : Path — contains the Crane's list of steps for navigating container

methods:

.new(@path)

Instantiates Crane class.

arguments:

  • @path : Path, required — a list of steps for navigating container

returns:

  • a new Crane instance

example:

my $crane = Crane.new('legumes', 0);

Crane classes get access to all methods documented above, with the stipulation that the path to the source container is the Crane instance's .path attribute.


Licensing

This is free and unencumbered public domain software. For more information, see http://unlicense.org/ or the accompanying UNLICENSE file.