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
add, remove, replace, move, copy, test operations
get/set
diff/patch
list the contents of a nested data structure in accessible format
METHODS
.at($container,*@path)>
.in($container,*@path)>
.exists($container,:@path!,:$k,:$v)>
.get($container,:@path!,:$k,:$v,:$p)>
.set($container,:@path!,:$value!)>
.add($container,:@path!,:$value!,:$in-place)>
.remove($container,:@path!,:$in-place)>
.replace($container,:@path!,:$value!,:$in-place)>
.move($container,:@from!,:@path!,:$in-place)>
.copy($container,:@from!,:@path!,:$in-place)>
.test($container,:@path!,:$value!)>
.list($container,:@path)>
.flatten($container,:@path)>
.transform($container,:@path!,:&with!,:$in-place)>
.patch($container,@patch,:$in-place)>
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:
$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>
});
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:
$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> });
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:
$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.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.
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:
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:
$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.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:
$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.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:
$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.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:
$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.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:
$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:
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"
}
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
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.