Rand Stats

Crane

zef:raku-community-modules

Actions Status Actions Status Actions Status

NAME

Crane - navigate Raku 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).raku;
(
    {:path(["a", 0]), :value("alligators")},
    {:path(["b", 0]), :value("be")},
    {:path(["d", 0]), :value("dangerous")}
)

DESCRIPTION

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

FEATURES

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")
        }
    ]);

.at($container,*@path)

Navigates to and returns container is rw.

arguments:

returns:

example:

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

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

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

.in($container,*@path)

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

Crane.at(my %h, qw<a b c>) = 5 # ✗ Crane error: associative key does not exist
Crane.in(my %i, qw<a b c>) = 5
say %i.raku;
{
    :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;
Crane.in(%h, qw<a b>, 0, *-1) = 'here';
say %h.raku;
{
    :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>})}) ];
Crane.in(@a, 0, 'i', 'want', 2) = 'always';
say @a.raku;
[
    :i({
        :want({
            "2" => "always",
            :my("MTV")
        })
    })
]

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

arguments:

returns:

example:

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

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

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

arguments:

returns:

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:

returns:

example:

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

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

my $value-p = Crane.get(%data, :path('legumes', 1), :p);
say $value-p.raku; # 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.

.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:

returns:

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.raku; # { :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:

returns:

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.raku; # []
say @b.raku; # ["foo"]

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

Removes the pair at path from Associative types, similar to the Raku 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:

returns:

example:

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

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.raku; [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:

returns:

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.raku; # { :a<aaa>, :b<bbb>, :c<ccc> }
say %b.raku; # { :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:

returns:

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:

returns:

example:

my %h = :example<hello>;
my %h2 = Crane.copy(%h, :from(['example']), :path(['sample']));
say %h.raku; # { :example("hello") }
say %h2.raku; # { :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:

returns:

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:

returns:

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:

returns:

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:

returns:

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:

returns:

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

AUTHOR

Andy Weidenbaum

COPYRIGHT AND LICENSE

Copyright 2016 - 2022 Andy Weidenbaum

Copyright 2024 Raku Community

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