-module(glazer). -moduledoc """ Fast JSON/YAML/CSV encoding and decoding using the glaze C++ library. The public API is split across [`glazer_json`](`glazer_json`), [`glazer_yaml`](`glazer_yaml`), and [`glazer_csv`](`glazer_csv`). By default `null`s are represented as the atom `null`. To change it application-wide, set the `null` env key in your config: **Erlang**: ```erlang {glazer, [{null, nil}]}. ``` **Elixir**: ```elixir config :glazer, null: nil ``` """. -export([encode_integer/1, decode_integer/1, try_decode_integer/1, compile_path/1, find/2, info/0]). -type path_step() :: {field, binary()} | iterate | {index, integer()}. -type path() :: [path_step()]. -export_type([path/0]). -export([json_try_decode/1, json_try_decode/2, yaml_try_decode/1, yaml_try_decode/2, yaml_encode/1, yaml_encode/2, csv_try_decode/1, csv_try_decode/2, csv_encode/1, csv_encode/2, json_encode/1, json_encode/2, json_minify/1, json_prettify/1, json_query/2, json_query/3, json_scan/1, json_scan/2, format_error/2]). -on_load(init/0). -define(LIBNAME, glazer). -define(NOT_LOADED_ERROR, erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]})). -doc false. init() -> NullVal = application:get_env(?LIBNAME, null, null), is_atom(NullVal) orelse erlang:error("glazer: option 'null' must be an atom"), SoName = case code:priv_dir(?LIBNAME) of {error, bad_name} -> case code:which(?MODULE) of Filename when is_list(Filename) -> Dir = filename:dirname(filename:dirname(Filename)), filename:join([Dir, "priv", ?LIBNAME]); _ -> filename:join("../priv", ?LIBNAME) end; Dir -> filename:join(Dir, ?LIBNAME) end, erlang:load_nif(SoName, [{null, NullVal}]). -doc false. json_try_decode(_Input) -> ?NOT_LOADED_ERROR. -doc false. json_try_decode(_Input, _Opts) -> ?NOT_LOADED_ERROR. -doc false. yaml_try_decode(_Input) -> ?NOT_LOADED_ERROR. -doc false. yaml_try_decode(_Input, _Opts) -> ?NOT_LOADED_ERROR. -doc false. yaml_encode(_Data) -> ?NOT_LOADED_ERROR. -doc false. yaml_encode(_Data, _Opts) -> ?NOT_LOADED_ERROR. -doc false. csv_try_decode(_Input) -> ?NOT_LOADED_ERROR. -doc false. csv_try_decode(_Input, _Opts) -> ?NOT_LOADED_ERROR. -doc false. csv_encode(_Data) -> ?NOT_LOADED_ERROR. -doc false. csv_encode(_Data, _Opts) -> ?NOT_LOADED_ERROR. -doc false. json_encode(_Data) -> ?NOT_LOADED_ERROR. -doc false. json_encode(_Data, _Opts) -> ?NOT_LOADED_ERROR. -doc false. json_minify(_Input) -> ?NOT_LOADED_ERROR. -doc false. json_prettify(_Input) -> ?NOT_LOADED_ERROR. -doc false. json_query(_Input, _Filter) -> ?NOT_LOADED_ERROR. -doc false. json_query(_Input, _Filter, _DecodeOpts) -> ?NOT_LOADED_ERROR. -doc false. json_scan(_Bin) -> ?NOT_LOADED_ERROR. -doc false. json_scan(_Bin, _ScanState) -> ?NOT_LOADED_ERROR. -doc """ Encode an integer to its JSON string representation. Raises `badarg` if `Int` is not an integer. """. -spec encode_integer(integer()) -> binary(). encode_integer(_Int) -> ?NOT_LOADED_ERROR. -doc """ Decode a JSON number string to an integer. Raises `invalid_number_format` on invalid input. """. -spec decode_integer(binary() | iolist()) -> integer(). decode_integer(NumberString) -> case try_decode_integer(NumberString) of {ok, Int} -> Int; {error, Reason} -> error(Reason) end. -doc """ Decode a JSON number string to an integer, returning `{ok, Int}` or `{error, invalid_number_format}` instead of raising. """. -spec try_decode_integer(binary() | iolist()) -> {ok, integer()} | {error, invalid_number_format}. try_decode_integer(_NumberString) -> ?NOT_LOADED_ERROR. -doc """ Format an error message with `io_lib:format/2` and flatten to a binary. """. -spec format_error(io:format(), [term()]) -> binary(). format_error(Format, Args) -> iolist_to_binary(io_lib:format(Format, Args)). -doc """ Compile a [jq](https://jqlang.org/)-style path expression into a `path()` for use with `find/2`. Supports a small subset of jq syntax: - `.` - identity (returns the input term itself) - `.foo`, `.foo.bar` - field access (map key) - `.["foo bar"]` - bracketed field access, for keys with special characters - `.[]` - iterate: every element of a list, or every value of a map - `.[N]`, `.[-N]` - index into a list (negative indices count from the end) Segments can be chained freely, e.g. `.a.b[].c[0]`. Raises `{invalid_path, Filter}` if `Filter` doesn't match this grammar. ## Example ```erlang 1> glazer:compile_path(<<".a[].b">>). [{field,<<"a">>},iterate,{field,<<"b">>}] ``` """. -spec compile_path(binary() | iolist()) -> path(). compile_path(Filter) -> case nif_compile_path(iolist_to_binary(Filter)) of {ok, Path} -> Path; {error, Reason} -> error(Reason) end. -spec nif_compile_path(binary()) -> {ok, path()} | {error, {invalid_path, binary()}}. nif_compile_path(_Filter) -> ?NOT_LOADED_ERROR. -doc """ Return build information about the loaded NIF library: - `app_version`: the `vsn` from `glazer.app.src` that this build was made from (e.g. `<<"0.5.9">>`) - `version`: `git describe` of the checkout the NIF was built from (e.g. `<<"0.5.9-3-abc123">>`, with a trailing `*` if the working tree was dirty at build time) - `pgo`: `true` if built with profile-guided optimisation (`make optimize` / `make PGO=use`) - `optimization`: `none`, `'O1'` (debug/ASan builds), or `'O3'` (release) ## Example ```erlang 1> glazer:info(). #{app_version => <<"0.5.9">>, version => <<"0.5.9-3-abc123">>, pgo => true, optimization => 'O3'} ``` """. -spec info() -> #{app_version => binary(), version => binary(), pgo => boolean(), optimization => none | 'O1' | 'O3'}. info() -> nif_info(). -spec nif_info() -> map(). nif_info() -> ?NOT_LOADED_ERROR. -doc """ Find value(s) in `Term` by walking `Path`. `Term` is typically a decoded JSON/YAML document: nested maps and lists. `Path` is either a `path()` produced by `compile_path/1`, or a raw jq-style filter string (compiled on the fly via `compile_path/1` — raises `{invalid_path, Filter}` if it doesn't parse). As a string, `Path` supports a small subset of [jq](https://jqlang.org/) syntax (see `compile_path/1` for the full grammar), e.g. `.a.b[].c[0]`. Returns the list of values found at the end of `Path`. An empty list means no match. `.[]` steps fan out over every element of a list (or every value of a map), so a path containing `.[]` can produce multiple results. ## Examples ```erlang 1> Doc = #{<<"a">> => [#{<<"b">> => 1}, #{<<"b">> => 2}, #{<<"c">> => 3}]}. 2> glazer:find(Doc, <<".a[].b">>). [1, 2] 3> glazer:find(Doc, <<".a[2].c">>). [3] 4> glazer:find(Doc, <<".a[-1].c">>). [3] 5> glazer:find(Doc, <<".">>). [Doc] 6> glazer:find(#{<<"foo bar">> => 1}, <<".[\"foo bar\"]">>). [1] ``` """. -spec find(term(), path() | binary()) -> [term()]. find(Term, Filter) when is_binary(Filter) -> find_path(Term, compile_path(Filter)); find(Term, Path) when is_list(Path) -> find_path(Term, Path). find_path(Term, []) -> [Term]; find_path(Term, [iterate | Rest]) when is_list(Term) -> [Found || Elem <- Term, Found <- find_path(Elem, Rest)]; find_path(Term, [iterate | Rest]) when is_map(Term) -> [Found || Value <- maps:values(Term), Found <- find_path(Value, Rest)]; find_path(Term, [{index, Index} | Rest]) when is_list(Term) -> Pos = if Index < 0 -> length(Term) + Index + 1; true -> Index + 1 end, case Pos >= 1 andalso Pos =< length(Term) of true -> find_path(lists:nth(Pos, Term), Rest); false -> [] end; find_path(Term, [{field, Key} | Rest]) when is_map(Term) -> case maps:find(Key, Term) of {ok, Value} -> find_path(Value, Rest); error -> %% Maps decoded with {keys, atom | existing_atom} have atom keys; %% fall back to looking up Key as an existing atom. try binary_to_existing_atom(Key, utf8) of Atom -> case maps:find(Atom, Term) of {ok, Value} -> find_path(Value, Rest); error -> [] end catch error:badarg -> [] end end; find_path(_Term, _Path) -> [].