Skip to content

Latest commit

 

History

History
789 lines (709 loc) · 24.5 KB

README.md

File metadata and controls

789 lines (709 loc) · 24.5 KB

kvlists

Overview

kvlists is a module that can manipulate lists of key/value pairs in Erlang. It should be quite useful when dealing with medium to large-sized nested property lists or decoded JSON documents (in the format used by jsx). Its interface is similar to that of the proplists module, with the addition of nested key (path) retrieval and modification (loosely inspired by Bob Ippolito's great kvc library, which itself was inspired by Apple's NSKeyValueCoding protocol from Objective-C). kvlists provides functionality that is similar to that of XPath, but with a syntax specifically adapted to Erlang. It supports lists of key/value pairs where the keys are either atoms or binaries and their type specification is:

[{Key :: atom() | binary(), Value :: term()}]

Where Value can also be a nested list of key/value tuples.

Requirements

You should use a recent version of Erlang/OTP (the project has only been tested with Erlang R16B and 17.x so far). You also need GNU make and a recent version of rebar in the system path.

Installation

Pull the project from GitHub by running:

git pull https://github.com/jcomellas/kvlists.git

To compile the module you simply run make from the project's directory and to execute the unit tests you run make test. To build the (very) limited documentation run make doc.

If you want to have the kvlists application available after the module is compiled, insert it into the Erlang lib directory (e.g. by symlinking or copying) by running:

sudo ln -s . /usr/lib/erlang/lib/kvlists-`git describe --tags`

Status

This application has been lightly tested so far and is still in a development stage. It has a test suite that covers most of the functionality but it has not been used in production yet.

Type Specifications

The type specifications exported by the module are:

-type element_id() :: atom() | binary().
-type key()        :: atom() | binary().
-type value()      :: term().
-type kv()         :: {key(), value()}.
-type kvlist()     :: [kv()].
-type path_key()   :: key() | non_neg_integer() | {key(), element_id()}.
-type path()       :: [path_key()] | path_key().

Functions

The kvlists module also provides the following functions:


delete_path/2

Deletes the element that matches a Path (list of nested keys) in a nested List of key/value pairs. Each key in the Path can be a name (atom() or binary()); a positive integer (using 1-based indexing); or a tuple that looks like {Key, ElementId}. If the latter path key is used, then the function will try to match the element (tuple) in a list whose Key has the value ElementId and continue with the following path key. If no value is found corresponding to the Path then List is returned. If the Path is set to [] then [] will be returned.

Specification

-spec delete_path(Path :: path(), List :: kvlist()) -> value().

Examples

Given:

1> List = [{transactions,
            [{period, <<"3 months">>},
             {total, 3659},
             {completed, 3381},
             {canceled, 278},
             {ratings, [[{type, positive}, {percent, 99}],
                        [{type, negative}, {percent, 0}],
                        [{type, neutral}, {percent, 1}]]}]}].

Delete an empty key:

2> kvlists:delete_path([], List).
[]

Delete a key with a single (scalar) value:

3> kvlists:delete_path([transactions, total], List).
[{transactions,[{period,<<"3 months">>},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}]]}]}]

Delete a non-existent key:

4> kvlists:delete_path(invalid_key, List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}]]}]}]

Delete a nested key with a list of key/value pair lists:

5> kvlists:delete_path([transactions, ratings], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278}]}]

Delete some key/value pairs associated to a nested key by name and index:

6> kvlists:delete_path([transactions, ratings, 2], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,neutral},{percent,1}]]}]}]

Delete a single value associated to a nested key by name and index:

7> kvlists:delete_path([transactions, ratings, 3, percent], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral}]]}]}]

Delete multiple values associated to a nested key present in several elements of a list:

8> kvlists:delete_path([transactions, ratings, percent], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive}],
                          [{type,negative}],
                          [{type,neutral}]]}]}]

Delete an element in a list by element ID:

9> kvlists:delete_path([transactions, ratings, {type, negative}], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,neutral},{percent,1}]]}]}]

Delete a value from an element in a list by element ID:

10> kvlists:delete_path([transactions, ratings, {type, neutral}, percent], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral}]]}]}]

delete_value/2

Deletes all entries associated with Key from List.

Specification

-spec delete_value(Key :: key(), List :: kvlist()) -> kvlist().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Delete the value corresponding to the def key in the list:

[{abc,123},{ghi,789}] = kvlists:delete_value(def, List).

equal/2

Returns a boolean value indicating that two key-value lists are equal. Two lists are equal when they have the same keys and when those keys have the same values, independently of the order of the keys in the list.

Specification

-spec equal(List1 :: kvlist(), List2 :: kvlist()) -> boolean().

Example

Given:

List1 = [{abc, 123}, {def, 456}, {ghi, 789}].
List2 = [{abc, 123}, {ghi, 789}, {def, 456}].

Check that the two kvlists are equal independently of the order of their elements:

true = kvlists:equal(List1, List2).

Given

List3 = [{abc, 100}, {def, 456}, {ghi, 789}].

Check that two kvlists are not equal when the value of one of its elements is different:

false = kvlists:equal(List1, List3).

get_path/2

Performs the lookup of a Path (list of nested keys) over a nested List of key/value pairs. Each key in the Path can be a name (atom() or binary()); a positive integer (using 1-based indexing); or a tuple that looks like {Key, ElementId}. If the latter path key is used, then the function will try to match the element (tuple) in a list whose Key has the value ElementId and continue the lookup with the following path key. If no value is found corresponding to the Path then [] is returned.

Specification

-spec get_path(Path :: path(), List :: kvlist()) -> value().

Examples

Given:

1> List = [{transactions,
            [{period, <<"3 months">>},
             {total, 3659},
             {completed, 3381},
             {canceled, 278},
             {ratings, [[{type, positive}, {percent, 99}],
                        [{type, negative}, {percent, 0}],
                        [{type, neutral}, {percent, 1}]]}]}].

Retrieve an empty key:

2> kvlists:get_path([], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}]]}]}]

Retrieve a key with a single (scalar) value:

3> kvlists:get_path([transactions, total], List).
3659

Retrieve a non-existent key:

4> kvlists:get_path(invalid_key, List).
[]

Retrieve a nested key with a list of key/value pair lists:

5> kvlists:get_path([transactions, ratings], List).
[[{type, positive}, {percent, 99}],
 [{type, negative}, {percent, 0}],
 [{type, neutral}, {percent, 1}]].

Retrieve some key/value pairs associated to a nested key by name and index:

6> kvlists:get_path([transactions, ratings, 2], List).
[{type, negative}, {percent, 0}].

Retrieve a single value associated to a nested key by name and index:

7> kvlists:get_path([transactions, ratings, 3, percent], List).
1

Retrieve multiple values associated to a nested key present in several elements of a list:

8> kvlists:get_path([transactions, ratings, percent], List).
[99,0,1]

Retrieve multiple values associated to a nested key present in several elements of a list:

9> kvlists:get_path([transactions, ratings, type], List).
[positive,negative,neutral]

Retrieve an element in a list by element ID:

10> kvlists:get_path([transactions, ratings, {type, negative}], List).
[{type,negative},{percent,0}]

Retrieve a value from an element in a list by element ID:

11> kvlists:get_path([transactions, ratings, {type, neutral}, percent], List).
1

get_value/2

Equivalent to get_value(Key, List, undefined).

Specification

-spec get_value(Key :: key(), List :: kvlist()) -> value() | undefined.

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Retrieve the value for key ghi:

789 = kvlists:get_value(ghi, List).

Retrieve the value for a non-existent key:

undefined = kvlists:get_value(jkl, List).

get_value/3

Returns the value of a simple key/value property in List. If the Key is found in the list, this function returns the corresponding Value, otherwise Default is returned.

Specification

-spec get_value(Key :: path_key(), List :: kvlist(), Default :: value()) -> value().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Retrieve the value for key ghi:

789 = kvlists:get_value(ghi, List, 100).

Retrieve the value for key jkl:

100 = kvlists:get_value(jkl, List, 100).

get_values/2

Returns the list of values corresponding to the different Keys in List. If the entry in Keys is found in the List, this function returns the corresponding value. If the entry is not found and it's a {Key, Default} tuple, Default is added to the returned list in its place and if the entry is just a key, then undefined is added to the returned list.

Specification

-spec get_values([Key :: path_key() | {Key :: path_key(), Default :: value()}],
                 List :: kvlist()) -> Values :: [value()].

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Retrieve the value for key ghi:

[789] = kvlists:get_values([ghi], List).

Retrieve multiple values with some of them set to defaults:

[123, 456, 789, 200] = kvlists:get_values([abc, {def, 100}, ghi, {jkl, 200}], List).

match/2

Matches a list of key-value pairs against a Pattern passed as a kvlist where optional keys, values or key-value pairs can be matched using the '_' atom as wildcard. It returns a boolean value indicating that the match was successful.

The Pattern is evaluated sequentially from head to tail, but the key-value pairs in the List need not follow the same order that was used in the Pattern to match successfully.

The wildcard atom '_' will match any expression at the nesting level where it was added in the pattern. Some of the ways in which it can be used are:

If you want to match... Pattern
any value for key foo {foo, '_'}
all keys with a single value {'_', Value :: term()}
any key-value pair {'_', '_'}
anything (including key-value pairs) '_'

The matching rules are applied recursively when the kvlist is nested.

Specification

-spec match(Pattern :: kvlist(), List :: kvlist()) -> boolean().

Example

Given:

SimpleList = [{abc, 111}, {def, 222}, {ghi, 222}].

Match the list against the trivial wildcard pattern:

true = kvlists:match('_', SimpleList).

Match the list against the tuple wildcard pattern:

true = kvlists:match([{'_', '_'}], SimpleList).

Match the list against a pattern where an element with an incorrect value is checked:

false = kvlists:match([{def, 333}, '_'], SimpleList).

Match the list against a pattern where two elements are matched exactly and another one is allowed to have any value:

true = kvlists:match([{def, 222}, {abc, '_'}, {ghi, 222}], SimpleList).

Match the list against itself:

true = kvlists:match(SimpleList, SimpleList).

Given:

NestedList = [{transactions,
               [{period, <<"3 months">>},
                {total, 3659},
                {completed, 3381},
                {canceled, 278},
                {ratings, [[{type, positive}, {percent, 99}],
                           [{type, negative}, {percent, 0}],
                           [{type, neutral}, {percent, 1}]]}]}].

Match the list against a complex pattern:

true = kvlists:match([{transactions, [{canceled, '_'},
                                      {total, 3659},
                                      {ratings, [[{type, positive}, {percent, '_'}], '_']},
                                      {'_', '_'}]}], NestedList).

member/2

Returns true if there is an entry in List whose key is equal to Key, otherwise false.

Specification

-spec member(Key :: key(), List :: kvlist()) -> boolean().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Check that a key is present in the list:

true = kvlists:member(ghi, List).

Check that a key is not present in the list:

false = kvlists:member(jkl, List).

set_path/3

Assigns a Value to the element in a List of key/value pairs corresponding to the Path that was passed. The Path can be a sequence of: names (atom() or binary()); indexes (1-based); or a tuple that looks like {Key, ElementId}. If the latter path key is used, then the function will try to match the element (tuple) in a list whose Key has the value ElementId and continue the lookup with the following path key.

Specification

-spec set_path(Path :: path(), Value :: value(), List :: kvlist()) -> kvlist().

Examples

Given:

1> List = [{transactions,
            [{period, <<"3 months">>},
             {total, 3659},
             {completed, 3381},
             {canceled, 278},
             {ratings, [[{type, positive}, {percent, 99}],
                        [{type, negative}, {percent, 0}],
                        [{type, neutral}, {percent, 1}]]}]}].

Set a value with an empty path:

2> kvlists:set_path([], <<"6 months">>, List).
<<"6 months">>

Set a value by key name:

3> kvlists:set_path([transactions, period], <<"6 months">>, List).
[{transactions,[{period,<<"6 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}]]}]}]

Set individual value by key name and index in list:

4> kvlists:set_path([transactions, ratings, 2, percent], 55, List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,55}],
                          [{type,neutral},{percent,1}]]}]}]

Set multiple entries in a list with a single value:

5> kvlists:set_path([transactions, ratings, percent], 123, List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,123}],
                          [{type,negative},{percent,123}],
                          [{type,neutral},{percent,123}]]}]}]

Set multiple entries in a list with a multiple values:

6> kvlists:set_path([transactions, ratings, percent], [10, 20, 30, 40], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,10}],
                          [{type,negative},{percent,20}],
                          [{type,neutral},{percent,30}],
                          [{percent,40}]]}]}]

Set a single value in list by element ID:

7> kvlists:set_path([transactions, ratings, {type, positive}, percent], 1000, List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,1000}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}]]}]}]

Replace an element by element ID:

8> kvlists:set_path([transactions, ratings, {type, negative}], [{type, unknown}, {value, 100}], List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,unknown},{value,100}],
                          [{type,neutral},{percent,1}]]}]}]

Add a new element by element ID:

9> kvlists:set_path([transactions, ratings, {type, unknown}, percent], 50, List).
[{transactions,[{period,<<"3 months">>},
                {total,3659},
                {completed,3381},
                {canceled,278},
                {ratings,[[{type,positive},{percent,99}],
                          [{type,negative},{percent,0}],
                          [{type,neutral},{percent,1}],
                          [{type,unknown},{percent,50}]]}]}]

set_value/3

Adds a property to the List with the corresponding Key and Value.

Specification

-spec set_value(Key :: path_key(), Value :: value(), List :: kvlist()) -> kvlist().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Set the value of the key def to 200 in the list:

[{abc, 123}, {def, 200}, {ghi, 789}] = kvlists:set_value(def, 200, List).

set_values/2

Sets each Key in List to its corresponding Value.

Specification

-spec set_values([{Key :: path_key(), Value :: value()}], List :: kvlist()) ->
                        NewList :: kvlist().

Example

1> List = [{abc, 123}, {def, 456}, {ghi, 789}].
2> kvlists:set_values([{abc, 100}, {jkl, <<"JKL">>}], List).
[{abc, 100}, {def, 456}, {ghi, 789}, {jkl, <<"JKL">>}]

take_value/2

Equivalent to take_value(Key, List, undefined).

Specification

-spec take_value(Key :: key(), List :: kvlist()) -> {value() | undefined, NewList :: kvlist()}.

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Take the value of the key ghi out of the list:

{789, [{abc, 123}, {def, 456}]} = kvlists:take_value(ghi, List).

Take the value of a non-existent key out of the list:

{undefined, [{abc, 123}, {def, 456}, {ghi, 789}]} = kvlists:take_value(jkl, List).

take_value/3

Returns the value of a simple key/value property in List and a NewList with the corresponding tuple removed. If the Key is found in the list, this function returns the corresponding Value, otherwise Default is returned in a tuple with the original List.

Specification

-spec take_value(Key :: path_key(), List :: kvlist(), Default :: value()) -> {value(), NewList :: kvlist()}.

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Take the value of the key ghi out of the list:

{789, [{abc, 123}, {def, 456}]} = kvlists:take_value(ghi, List, 100).

Take the value of a non-existent key out of the list, assigning a default value to it:

{100, [{abc, 123}, {def, 456}, {ghi, 789}]} = kvlists:take_value(jkl, List, 100).

take_values/2

Returns a tuple with the list of values corresponding to the different Keys in List and a NewList with those key/value pairs removed. If the entry in Keys is found in the List, this function will add the corresponding value to the list of values removed. If the entry is not found and it's a {Key, Default} tuple, Default is added to the returned list in its place. Finally, if the entry is just a key and is not found, then undefined is added to the returned list.

Specification

-spec take_values([Key :: path_key() | {Key :: path_key(), Default :: value()}],
                 List :: kvlist()) -> {Values :: [value()], NewList :: kvlist()}.

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Take the value of the key ghi out of the list:

{[789], [{abc, 123}, {def, 456}]} = kvlists:take_values([ghi], List).

Take the values of several keys out of the list, with some of them using default values:

{[123, 456, 789, 200], []} = kvlists:take_values([abc, {def, 100}, ghi, {jkl, 200}], List).

with/2

Return a NewList where the Key of each element is present in the list of Keys.

Specification

-spec with(Keys :: [key()], List :: kvlist()) -> NewList :: kvlist().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Return the list with only the abc and ghi keys in it:

[{abc, 123}, {ghi, 789}] = kvlists:with([abc, ghi], List).

without/2

Return a NewList where the Key of each element is not present in the list of Keys.

Specification

-spec without(Keys :: [key()], List :: kvlist()) -> NewList :: kvlist().

Example

Given:

List = [{abc, 123}, {def, 456}, {ghi, 789}].

Return the list without the abc and ghi keys:

[{def, 456}] = kvlists:without([abc, ghi], List).