# Copyright (c) 2025-2026 Voxgig Ltd. MIT LICENSE. # # Voxgig Struct — Elixir port. # # A faithful port of the canonical TypeScript implementation # (typescript/src/StructUtility.ts). The canonical algorithm mutates nodes in # place and relies on reference-stable nodes (shared references seen by walk / # merge / inject). The BEAM has no mutable, reference-stable native collection, # so this port emulates a small mutable heap with ETS (an OTP-stdlib facility, # like the JVM heap the Clojure port uses or Rust's Rc): a node is a # tagged reference `{:vmap, id}` / `{:vlist, id}` whose contents live in the # heap table and are replaced on mutation; the reference is stable, so all # holders observe updates. # # Like the Python / Clojure / Dart ports, Elixir has a single `nil`, so the # canonical `undefined` and JSON `null` are both `nil`; the Group A/B rules # recover the distinction, and a NOARG sentinel marks "no argument supplied". # # Zero third-party runtime dependencies (ETS and :re/Regex are OTP stdlib). defmodule Voxgig.Struct do @moduledoc "Faithful Elixir port of voxgig/struct. See AGENTS.md." # --------------------------------------------------------------------------- # Mutable heap (ETS) # --------------------------------------------------------------------------- @heap :vox_struct_heap defp ensure_heap do case :ets.whereis(@heap) do :undefined -> try do :ets.new(@heap, [:set, :public, :named_table]) rescue ArgumentError -> :ok end :ok _ -> :ok end end defp alloc(contents) do ensure_heap() id = :erlang.unique_integer([:positive, :monotonic]) :ets.insert(@heap, {id, contents}) id end defp hget(id) do case :ets.lookup(@heap, id) do [{_, c}] -> c _ -> nil end end defp hset(id, contents), do: :ets.insert(@heap, {id, contents}) defp vmap_new(pairs), do: {:vmap, alloc(pairs)} defp vlist_new(items), do: {:vlist, alloc(items)} defp empty_map, do: vmap_new([]) defp empty_list, do: vlist_new([]) defp map_pairs({:vmap, id}), do: hget(id) defp map_set_pairs({:vmap, id}, pairs), do: hset(id, pairs) defp list_items({:vlist, id}), do: hget(id) defp list_set_items({:vlist, id}, items), do: hset(id, items) # Injection cell defp vinj_new(fields), do: {:vinj, alloc(fields)} defp ig({:vinj, id}, k), do: Map.get(hget(id), k) defp is_({:vinj, id}, k, v) do hset(id, Map.put(hget(id), k, v)) v end defp isinj({:vinj, _}), do: true defp isinj(_), do: false # --------------------------------------------------------------------------- # Sentinels / constants # --------------------------------------------------------------------------- @noarg :vox_noarg def noarg, do: @noarg @skip :vox_skip @delete :vox_delete def skip, do: @skip def delete, do: @delete def is_skip(v), do: v == @skip def is_delete(v), do: v == @delete @m_keypre "key:pre" @m_keypost "key:post" @m_val "val" def m_keypre, do: 1 def m_keypost, do: 2 def m_val, do: 4 defp mode_to_num(@m_keypre), do: 1 defp mode_to_num(@m_keypost), do: 2 defp mode_to_num(@m_val), do: 4 defp mode_to_num(_), do: 0 @s_dkey "$KEY" @s_banno "`$ANNO`" @s_dtop "$TOP" @s_derrs "$ERRS" @s_dspec "$SPEC" @s_bexact "`$EXACT`" @s_bval "`$VAL`" @s_bkey "`$KEY`" @s_bopen "`$OPEN`" @s_mt "" @s_bt "`" @s_ds "$" @s_cn ":" @s_key "KEY" @s_viz ": " @t_any 0x7FFFFFFF @t_noval 0x40000000 @t_boolean 0x20000000 @t_decimal 0x10000000 @t_integer 0x08000000 @t_number 0x04000000 @t_string 0x02000000 @t_function 0x01000000 @t_null 0x00400000 @t_list 0x00004000 @t_map 0x00002000 @t_instance 0x00001000 @t_scalar 0x00000080 @t_node 0x00000040 def t_any, do: @t_any def t_noval, do: @t_noval def t_boolean, do: @t_boolean def t_decimal, do: @t_decimal def t_integer, do: @t_integer def t_number, do: @t_number def t_string, do: @t_string def t_function, do: @t_function def t_null, do: @t_null def t_list, do: @t_list def t_map, do: @t_map def t_instance, do: @t_instance def t_scalar, do: @t_scalar def t_node, do: @t_node @typename { "any", "nil", "boolean", "decimal", "integer", "number", "string", "function", "symbol", "null", "", "", "", "", "", "", "", "list", "map", "instance", "", "", "", "", "scalar", "node" } @maxdepth 32 @r_inject_full ~r/^`(\$[A-Z]+|[^`]*)[0-9]*`$/ @r_inject_part ~r/`([^`]*)`/ @r_meta_path ~r/^([^$]+)\$([=~])(.+)$/ @r_transform_name ~r/`\$([A-Z]+)`/ @r_intkey ~r/^-?[0-9]+$/ # --------------------------------------------------------------------------- # Low-level helpers # --------------------------------------------------------------------------- defp is_intish(n) when is_integer(n), do: true defp is_intish(n) when is_float(n), do: n == Float.floor(n) defp is_intish(_), do: false defp num_to_string(n) when is_integer(n), do: Integer.to_string(n) defp num_to_string(n) when is_float(n) do cond do n == Float.floor(n) and abs(n) < 1.0e16 -> Integer.to_string(trunc(n)) true -> shortest_float(n) end end defp shortest_float(n) do s = :erlang.float_to_binary(n, [:short]) s end defp js_string(nil), do: "null" defp js_string(true), do: "true" defp js_string(false), do: "false" defp js_string(v) when is_number(v), do: num_to_string(v) defp js_string(v) when is_binary(v), do: v defp js_string(v) when is_function(v), do: "function" defp js_string(@skip), do: "skip" defp js_string(@delete), do: "delete" defp js_string({:vlist, _} = v) do list_items(v) |> Enum.map(fn x -> if x == nil, do: "", else: js_string(x) end) |> Enum.join(",") end defp js_string({:vmap, _}), do: "[object Object]" defp js_string(v), do: inspect(v) defp mapkey(k) when is_binary(k), do: k defp mapkey(k) when is_number(k), do: num_to_string(k) defp mapkey(k), do: js_string(k) defp to_int(k) when is_boolean(k), do: nil defp to_int(k) when is_integer(k), do: k defp to_int(k) when is_float(k), do: trunc(Float.floor(k)) defp to_int(k) when is_binary(k) do case Integer.parse(String.trim(k)) do {n, ""} -> n _ -> nil end end defp to_int(_), do: nil defp clz32(n0) do n = Bitwise.band(n0, 0xFFFFFFFF) if n == 0 do 32 else clz_loop(n, 0) end end defp clz_loop(n, r) do if Bitwise.band(n, 0x80000000) != 0 do r else clz_loop(Bitwise.band(Bitwise.bsl(n, 1), 0xFFFFFFFF), r + 1) end end # --------------------------------------------------------------------------- # Minor utilities # --------------------------------------------------------------------------- def isnode({:vmap, _}), do: true def isnode({:vlist, _}), do: true def isnode(_), do: false def ismap({:vmap, _}), do: true def ismap(_), do: false def islist({:vlist, _}), do: true def islist(_), do: false def isfunc(v), do: is_function(v) def iskey(k) when is_binary(k), do: k != "" def iskey(k) when is_boolean(k), do: false def iskey(k) when is_number(k), do: true def iskey(_), do: false def isempty(v \\ nil) do cond do v == nil -> true v == "" -> true islist(v) -> list_items(v) == [] ismap(v) -> map_pairs(v) == [] true -> false end end def getdef(v, alt), do: if(v == nil, do: alt, else: v) def typify(value \\ @noarg) do cond do value == @noarg -> @t_noval value == nil -> Bitwise.bor(@t_scalar, @t_null) is_boolean(value) -> Bitwise.bor(@t_scalar, @t_boolean) is_integer(value) -> Bitwise.bor(Bitwise.bor(@t_scalar, @t_number), @t_integer) is_float(value) -> typify_float(value) is_binary(value) -> Bitwise.bor(@t_scalar, @t_string) is_function(value) -> Bitwise.bor(@t_scalar, @t_function) islist(value) -> Bitwise.bor(@t_node, @t_list) ismap(value) -> Bitwise.bor(@t_node, @t_map) true -> Bitwise.bor(@t_node, @t_instance) end end defp typify_float(value) do cond do value != value -> @t_noval value == Float.floor(value) -> Bitwise.bor(Bitwise.bor(@t_scalar, @t_number), @t_integer) true -> Bitwise.bor(Bitwise.bor(@t_scalar, @t_number), @t_decimal) end end def typename(t \\ 0) do i = clz32(t) if i >= 0 and i < tuple_size(@typename), do: elem(@typename, i), else: elem(@typename, 0) end def size(v \\ nil) do cond do islist(v) -> length(list_items(v)) ismap(v) -> length(map_pairs(v)) is_binary(v) -> String.length(v) is_boolean(v) -> if v, do: 1, else: 0 is_number(v) -> trunc(Float.floor(v / 1)) true -> 0 end end def strkey(key \\ nil) do cond do key == nil -> @s_mt is_binary(key) -> key is_boolean(key) -> @s_mt is_number(key) -> if(is_intish(key), do: num_to_string(key), else: num_to_string(Float.floor(key))) true -> @s_mt end end def keysof(v \\ nil) do cond do ismap(v) -> map_pairs(v) |> Enum.map(&elem(&1, 0)) |> Enum.sort() islist(v) -> n = length(list_items(v)); if(n == 0, do: [], else: Enum.map(0..(n - 1), &Integer.to_string/1)) true -> [] end end defp omap_get(pairs, k) do case List.keyfind(pairs, k, 0) do {_, v} -> {:ok, v} nil -> :error end end defp omap_put(pairs, k, v) do if List.keymember?(pairs, k, 0) do Enum.map(pairs, fn {kk, vv} -> if kk == k, do: {k, v}, else: {kk, vv} end) else pairs ++ [{k, v}] end end defp omap_del(pairs, k), do: Enum.reject(pairs, fn {kk, _} -> kk == k end) def getprop(val, key, alt \\ nil) do if val == nil or key == nil do alt else out = cond do ismap(val) -> case omap_get(map_pairs(val), mapkey(key)) do {:ok, v} -> v :error -> nil end islist(val) -> ki = to_int(key) items = if ki == nil, do: [], else: list_items(val) if ki != nil and ki >= 0 and ki < length(items), do: Enum.at(items, ki), else: nil true -> nil end if out == nil, do: alt, else: out end end defp lookup_(val, key) do cond do val == nil or key == nil -> nil ismap(val) -> case omap_get(map_pairs(val), mapkey(key)) do {:ok, v} -> v :error -> nil end islist(val) -> ki = to_int(key) items = if ki == nil, do: [], else: list_items(val) if ki != nil and ki >= 0 and ki < length(items), do: Enum.at(items, ki), else: nil true -> nil end end def haskey(val \\ nil, key \\ nil), do: getprop(val, key) != nil def getelem(val, key, alt \\ nil) do if val == nil or key == nil do alt else out = if islist(val) do ks = cond do; is_binary(key) -> key; is_number(key) -> num_to_string(key); true -> "" end if Regex.match?(@r_intkey, ks) do items = list_items(val) len = length(items) nk0 = String.to_integer(ks) nk = if nk0 < 0, do: len + nk0, else: nk0 if nk >= 0 and nk < len, do: Enum.at(items, nk), else: nil else nil end else nil end if out == nil do if isfunc(alt), do: alt.(), else: alt else out end end end defp getprop_raw(v, k) do cond do ismap(v) -> case omap_get(map_pairs(v), k) do {:ok, x} -> x :error -> nil end islist(v) -> i = String.to_integer(k) items = list_items(v) if i >= 0 and i < length(items), do: Enum.at(items, i), else: nil true -> nil end end defp items_pairs(v) do if not isnode(v), do: [], else: Enum.map(keysof(v), fn k -> {k, getprop_raw(v, k)} end) end def items(v \\ nil) do vlist_new(Enum.map(items_pairs(v), fn {k, x} -> vlist_new([k, x]) end)) end defp items_v(v, f), do: vlist_new(Enum.map(items_pairs(v), f)) def flatten(l, depth \\ 1) do if not islist(l) do l else out = Enum.reduce(list_items(l), [], fn item, acc -> if islist(item) and depth > 0 do acc ++ list_items(flatten(item, depth - 1)) else acc ++ [item] end end) vlist_new(out) end end def filter(val, check) do out = Enum.reduce(items_pairs(val), [], fn {k, x}, acc -> if check.({k, x}), do: acc ++ [x], else: acc end) vlist_new(out) end def setprop(parent, key, val) do cond do not iskey(key) -> parent ismap(parent) -> map_set_pairs(parent, omap_put(map_pairs(parent), mapkey(key), val)) parent islist(parent) -> case to_int(key) do nil -> parent ki -> items = list_items(parent) len = length(items) new = cond do ki >= 0 -> ki2 = if ki > len, do: len, else: ki if ki2 >= len, do: items ++ [val], else: List.replace_at(items, ki2, val) true -> [val | items] end list_set_items(parent, new) parent end true -> parent end end def delprop(parent, key) do cond do not iskey(key) -> parent ismap(parent) -> map_set_pairs(parent, omap_del(map_pairs(parent), mapkey(key))) parent islist(parent) -> case to_int(key) do nil -> parent ki -> items = list_items(parent) if ki >= 0 and ki < length(items), do: list_set_items(parent, List.delete_at(items, ki)) parent end true -> parent end end def clone(v \\ nil) do cond do ismap(v) -> vmap_new(Enum.map(map_pairs(v), fn {k, x} -> {k, clone(x)} end)) islist(v) -> vlist_new(Enum.map(list_items(v), &clone/1)) true -> v end end def slice(val, start \\ nil, stop \\ nil, mutate \\ false) do cond do is_number(val) and not is_boolean(val) -> lo = if is_number(start), do: start, else: nil hi = if is_number(stop), do: stop - 1, else: nil cond do hi != nil and val > hi -> hi lo != nil and val < lo -> lo true -> val end islist(val) or is_binary(val) -> slice_seq(val, start, stop, mutate) true -> val end end defp slice_seq(val, start, stop, mutate) do vlen = size(val) start = if start == nil and stop != nil, do: 0, else: start if start == nil do val else s0 = trunc(start) {s, e} = cond do s0 < 0 -> e = vlen + s0 {0, if(e < 0, do: 0, else: e)} stop != nil -> e0 = trunc(stop) cond do e0 < 0 -> ee = vlen + e0; {s0, if(ee < 0, do: 0, else: ee)} vlen < e0 -> {s0, vlen} true -> {s0, e0} end true -> {s0, vlen} end s = if vlen < s, do: vlen, else: s if s > -1 and s <= e and e <= vlen do cond do islist(val) -> sub = Enum.slice(list_items(val), s, e - s) if mutate, do: (list_set_items(val, sub); val), else: vlist_new(sub) true -> binary_part(val, s, e - s) end else cond do islist(val) -> if mutate, do: (list_set_items(val, []); val), else: empty_list() true -> @s_mt end end end end # --------------------------------------------------------------------------- # Regex helpers (uniform re_* API over :re/Regex) # --------------------------------------------------------------------------- defp rx(p) do cond do is_struct(p, Regex) -> p is_binary(p) -> Regex.compile!(p) true -> Regex.compile!(js_string(p)) end end def re_compile(p, _flags \\ nil), do: rx(p) def re_test(p, input), do: Regex.match?(rx(p), if(is_binary(input), do: input, else: js_string(input))) def re_find(p, input) do case Regex.run(rx(p), if(is_binary(input), do: input, else: js_string(input))) do nil -> nil groups -> vlist_new(Enum.map(groups, fn g -> g || "" end)) end end def re_find_all(p, input) do matches = Regex.scan(rx(p), if(is_binary(input), do: input, else: js_string(input))) vlist_new(Enum.map(matches, fn groups -> vlist_new(Enum.map(groups, fn g -> g || "" end)) end)) end def re_replace(_p, input, _repl), do: input def re_escape(s), do: escre(s) def escre(s \\ nil) do str = cond do; is_binary(s) -> s; s == nil -> @s_mt; true -> js_string(s) end str |> String.to_charlist() |> Enum.map(fn c -> ch = <> if String.contains?(".*+?^${}()|[]\\", ch), do: "\\" <> ch, else: ch end) |> Enum.join("") end @url_unreserved "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()" def escurl(s \\ nil) do str = cond do; is_binary(s) -> s; s == nil -> @s_mt; true -> js_string(s) end :binary.bin_to_list(str) |> Enum.map(fn b -> ch = <> if String.contains?(@url_unreserved, ch) do ch else "%" <> String.upcase(String.pad_leading(Integer.to_string(b, 16), 2, "0")) end end) |> Enum.join("") end # --------------------------------------------------------------------------- # JSON-ish serialization / stringify / jsonify # --------------------------------------------------------------------------- defp esc_json(s) do inner = s |> String.to_charlist() |> Enum.map(fn c -> case c do ?" -> "\\\"" ?\\ -> "\\\\" ?\n -> "\\n" ?\r -> "\\r" ?\t -> "\\t" c when c < 32 -> "\\u" <> String.pad_leading(Integer.to_string(c, 16), 4, "0") c -> <> end end) |> Enum.join("") "\"" <> inner <> "\"" end defp json_encode(v, sort \\ false, indent \\ nil), do: json_enc(v, sort, indent, 0) defp json_enc(v, sort, indent, level) do cond do v == nil -> "null" v == true -> "true" v == false -> "false" is_number(v) -> num_to_string(v) is_binary(v) -> esc_json(v) is_function(v) -> "null" v == @skip or v == @delete -> "null" islist(v) -> json_list(list_items(v), sort, indent, level) ismap(v) -> json_map(map_pairs(v), sort, indent, level) true -> esc_json(js_string(v)) end end defp json_list([], _sort, _indent, _level), do: "[]" defp json_list(items, sort, nil, level) do "[" <> Enum.map_join(items, ",", fn x -> json_enc(x, sort, nil, level + 1) end) <> "]" end defp json_list(items, sort, indent, level) do pad = String.duplicate(" ", indent * (level + 1)) cpad = String.duplicate(" ", indent * level) body = Enum.map_join(items, ",\n", fn x -> pad <> json_enc(x, sort, indent, level + 1) end) "[\n" <> body <> "\n" <> cpad <> "]" end defp json_map(pairs, sort, indent, level) do ks = Enum.map(pairs, &elem(&1, 0)) ks = if sort, do: Enum.sort(ks), else: ks cond do ks == [] -> "{}" indent == nil -> "{" <> Enum.map_join(ks, ",", fn k -> {_, v} = List.keyfind(pairs, k, 0) esc_json(k) <> ":" <> json_enc(v, sort, nil, level + 1) end) <> "}" true -> pad = String.duplicate(" ", indent * (level + 1)) cpad = String.duplicate(" ", indent * level) body = Enum.map_join(ks, ",\n", fn k -> {_, v} = List.keyfind(pairs, k, 0) pad <> esc_json(k) <> ": " <> json_enc(v, sort, indent, level + 1) end) "{\n" <> body <> "\n" <> cpad <> "}" end end defp has_cycle(v), do: has_cycle(v, MapSet.new()) defp has_cycle({:vmap, id} = v, seen) do if MapSet.member?(seen, id), do: true, else: Enum.any?(map_pairs(v), fn {_, x} -> has_cycle(x, MapSet.put(seen, id)) end) end defp has_cycle({:vlist, id} = v, seen) do if MapSet.member?(seen, id), do: true, else: Enum.any?(list_items(v), fn x -> has_cycle(x, MapSet.put(seen, id)) end) end defp has_cycle(_, _), do: false def stringify(v \\ @noarg, maxlen \\ nil, pretty \\ nil) do pr = pretty == true cond do v == @noarg -> if pr, do: "<>", else: @s_mt true -> valstr = cond do is_binary(v) -> v has_cycle(v) -> "__STRINGIFY_FAILED__" true -> String.replace(json_encode(v, true), "\"", "") end valstr = if is_number(maxlen) and maxlen > -1 do m = trunc(maxlen) if m < String.length(valstr) do String.slice(valstr, 0, max(0, m - 3)) <> "..." else valstr end else valstr end if pr, do: stringify_pretty(valstr), else: valstr end end defp stringify_pretty(valstr) do colors = [81, 118, 213, 39, 208, 201, 45, 190, 129, 51, 160, 121, 226, 33, 207, 69] c = Enum.map(colors, fn n -> "\e[38;5;" <> Integer.to_string(n) <> "m" end) |> List.to_tuple() r = "\e[0m" clen = tuple_size(c) {_, _, t} = String.to_charlist(valstr) |> Enum.reduce({0, elem(c, 0), elem(c, 0)}, fn ch, {d, o, t} -> chs = <> cond do ch == ?{ or ch == ?[ -> d2 = d + 1 o2 = elem(c, rem(d2, clen)) {d2, o2, t <> o2 <> chs} ch == ?} or ch == ?] -> t2 = t <> o <> chs d2 = d - 1 o2 = elem(c, rem(rem(d2, clen) + clen, clen)) {d2, o2, t2} true -> {d, o, t <> o <> chs} end end) t <> r end def jsonify(v \\ nil, flags \\ nil) do if v == nil do "null" else indent = getprop(flags, "indent", 2) ind = if is_number(indent), do: trunc(indent), else: 2 str = if ind > 0, do: json_encode(v, false, ind), else: json_encode(v) offset = getprop(flags, "offset", 0) off = if is_number(offset), do: trunc(offset), else: 0 if off > 0 do lines = String.split(str, "\n") case lines do [_ | rest] -> "{\n" <> Enum.map_join(rest, "\n", fn l -> String.duplicate(" ", off) <> l end) [] -> str end else str end end end def pad(s \\ nil, padding \\ nil, padchar \\ nil) do str = cond do; is_binary(s) -> s; s == nil -> "null"; true -> stringify(s) end p = if is_number(padding), do: trunc(padding), else: 44 pc = if is_binary(padchar), do: String.slice(padchar <> " ", 0, 1), else: " " if p > -1 do n = p - String.length(str) if n > 0, do: str <> String.duplicate(pc, n), else: str else n = -p - String.length(str) if n > 0, do: String.duplicate(pc, n) <> str, else: str end end # --------------------------------------------------------------------------- # join / pathify / replace # --------------------------------------------------------------------------- def join(arr, sep \\ nil, url \\ nil) do if not islist(arr) do @s_mt else sepdef = cond do; sep == nil -> ","; is_binary(sep) -> sep; true -> js_string(sep) end single = String.length(sepdef) == 1 sc = if single, do: sepdef, else: " " is_url = url == true items = list_items(arr) sarr = length(items) out = items |> Enum.with_index() |> Enum.reduce([], fn {s0, idx}, acc -> if is_binary(s0) and s0 != @s_mt do s = if single do cond do is_url and idx == 0 -> strip_trailing(s0, sc) true -> x = if idx > 0, do: strip_leading(s0, sc), else: s0 x = if idx < sarr - 1 or not is_url, do: strip_trailing(x, sc), else: x collapse(x, sc) end else s0 end if s != @s_mt, do: acc ++ [s], else: acc else acc end end) Enum.join(out, sepdef) end end defp strip_trailing(s, sc) do if String.ends_with?(s, sc) and s != "", do: strip_trailing(String.slice(s, 0, String.length(s) - 1), sc), else: s end defp strip_leading(s, sc) do if String.starts_with?(s, sc) and s != "", do: strip_leading(String.slice(s, 1, String.length(s) - 1), sc), else: s end defp collapse(s, sc) do chars = String.graphemes(s) collapse_loop(chars, sc, []) end defp collapse_loop([], _sc, acc), do: Enum.join(Enum.reverse(acc), "") defp collapse_loop([ch | rest], sc, acc) do if ch != sc do collapse_loop(rest, sc, [ch | acc]) else {run, rest2} = take_run(rest, sc, [ch]) before_non = acc != [] and hd(acc) != sc after_non = rest2 != [] if before_non and after_non do collapse_loop(rest2, sc, [sc | acc]) else collapse_loop(rest2, sc, Enum.reverse(run) ++ acc) end end end defp take_run([ch | rest], sc, acc) when ch == sc, do: take_run(rest, sc, [ch | acc]) defp take_run(rest, _sc, acc), do: {Enum.reverse(acc), rest} def joinurl(arr), do: join(arr, "/", true) def replace(s, from, to) do ts = typify(s) rs = cond do Bitwise.band(@t_string, ts) == 0 -> stringify(s) Bitwise.band(Bitwise.bor(@t_noval, @t_null), ts) > 0 -> @s_mt true -> stringify(s) end to_s = if is_binary(to), do: to, else: js_string(to) cond do is_binary(from) and from != "" -> String.replace(rs, from, to_s) is_struct(from, Regex) -> Regex.replace(from, rs, to_s) true -> rs end end def pathify(v \\ @noarg, startin \\ nil, endin \\ nil) do absent = v == @noarg val = if absent, do: nil, else: v path = cond do islist(val) -> list_items(val) iskey(val) -> [val] true -> nil end start = if is_number(startin), do: if(startin > -1, do: trunc(startin), else: 0), else: 0 endn = if is_number(endin), do: if(endin > -1, do: trunc(endin), else: 0), else: 0 pathstr = if path != nil and start >= 0 do len = length(path) e = max(0, len - endn) s = if start > len, do: len, else: start sub = if s <= e, do: Enum.slice(path, s, e - s), else: [] if sub == [] do "" else sub |> Enum.filter(&iskey/1) |> Enum.map(fn p -> cond do is_integer(p) -> num_to_string(p) is_float(p) -> num_to_string(Float.floor(p)) true -> String.replace(js_string(p), ".", @s_mt) end end) |> Enum.join(".") end else nil end if pathstr == nil do " (if absent, do: @s_mt, else: @s_cn <> stringify(val, 47)) <> ">" else pathstr end end # --------------------------------------------------------------------------- # walk / merge # --------------------------------------------------------------------------- def walk(val, opts \\ []) do before = Keyword.get(opts, :before) aft = Keyword.get(opts, :after) maxdepth = Keyword.get(opts, :maxdepth) key = Keyword.get(opts, :key) parent = Keyword.get(opts, :parent) path = Keyword.get(opts, :path) path = if path == nil, do: empty_list(), else: path depth = size(path) out = if before == nil, do: val, else: before.(key, val, parent, path) md = if is_number(maxdepth) and maxdepth >= 0, do: trunc(maxdepth), else: @maxdepth if md == 0 or (md > 0 and md <= depth) do out else if isnode(out) do prefix = list_items(path) Enum.each(items_pairs(out), fn {ckey, child} -> childpath = vlist_new(prefix ++ [ckey]) result = walk(child, before: before, after: aft, maxdepth: md, key: ckey, parent: out, path: childpath) setprop(out, ckey, result) end) end if aft == nil, do: out, else: aft.(key, out, parent, path) end end defp grow(a, n) do if size(a) <= n do setprop(a, size(a), nil) grow(a, n) end end def merge(objs, maxdepth \\ nil) do md = if is_number(maxdepth), do: if(maxdepth < 0, do: 0, else: trunc(maxdepth)), else: @maxdepth if not islist(objs) do objs else items = list_items(objs) lenlist = length(items) cond do lenlist == 0 -> nil lenlist == 1 -> Enum.at(items, 0) true -> out0 = getprop(objs, 0, empty_map()) out = merge_loop(objs, lenlist, md, out0) if md == 0 do o = getprop(objs, lenlist - 1) cond do; islist(o) -> empty_list(); ismap(o) -> empty_map(); true -> o end else out end end end end defp merge_loop(_objs, _lenlist, _md, out, oi \\ 1) defp merge_loop(objs, lenlist, md, out, oi) do if oi >= lenlist do out else obj = getprop(objs, oi) out2 = if not isnode(obj) do obj else cur = vlist_new([out]) dst = vlist_new([out]) before = fn key, val, _parent, path -> pi = size(path) cond do md <= pi -> grow(cur, pi) setprop(cur, pi, val) if pi > 0, do: setprop(getelem(cur, pi - 1), key, val) nil not isnode(val) -> grow(cur, pi) setprop(cur, pi, val) val true -> grow(dst, pi) grow(cur, pi) dnew = if pi > 0, do: getprop(getelem(dst, pi - 1), key), else: getelem(dst, pi) setprop(dst, pi, dnew) tval = getelem(dst, pi) cond do tval == nil -> setprop(cur, pi, if(islist(val), do: empty_list(), else: empty_map())) val (islist(val) and islist(tval)) or (ismap(val) and ismap(tval)) -> setprop(cur, pi, tval) val true -> setprop(cur, pi, val) nil end end end aft = fn key, vv, _parent, path -> ci = size(path) if ci < 1 do if size(cur) > 0, do: getelem(cur, 0), else: vv else target = if ci - 1 < size(cur), do: getelem(cur, ci - 1), else: nil value = if ci < size(cur), do: getelem(cur, ci), else: nil setprop(target, key, value) value end end walk(obj, before: before, after: aft) end merge_loop(objs, lenlist, md, out2, oi + 1) end end # --------------------------------------------------------------------------- # getpath / setpath # --------------------------------------------------------------------------- defp idef(injdef, field) do cond do isinj(injdef) -> case field do "base" -> ig(injdef, :base) "dparent" -> ig(injdef, :dparent) "meta" -> ig(injdef, :meta) "key" -> ig(injdef, :key) "dpath" -> ig(injdef, :dpath) "handler" -> ig(injdef, :handler) end injdef == nil -> nil true -> getprop(injdef, field) end end defp dummy_inj, do: new_inj(nil, vmap_new([{@s_dtop, nil}])) def getpath(store, path, injdef \\ nil) do parts = cond do islist(path) -> list_items(path) is_binary(path) -> String.split(path, ".") is_number(path) and not is_boolean(path) -> [strkey(path)] true -> nil end if parts == nil do nil else has_inj = injdef != nil base = idef(injdef, "base") dparent = idef(injdef, "dparent") inj_meta = idef(injdef, "meta") inj_key = idef(injdef, "key") dpath = idef(injdef, "dpath") src = if iskey(base), do: getprop(store, base, store), else: store numparts = length(parts) val = cond do path == nil or store == nil or (numparts == 1 and Enum.at(parts, 0) == @s_mt) or numparts == 0 -> src true -> val0 = if numparts == 1, do: getprop(store, Enum.at(parts, 0)), else: store if isfunc(val0) do val0 else {val1, parts1} = case if(is_binary(Enum.at(parts, 0)), do: Regex.run(@r_meta_path, Enum.at(parts, 0)), else: nil) do [_, g1, _, g3] when inj_meta != nil and has_inj -> {getprop(inj_meta, g1), List.replace_at(parts, 0, g3)} _ -> {src, parts} end getpath_loop(store, parts1, 0, val1, has_inj, inj_key, inj_meta, src, dparent, dpath) end end handler = idef(injdef, "handler") if has_inj and isfunc(handler) do ref = pathify(path) if isinj(injdef), do: handler.(injdef, val, ref, store), else: handler.(dummy_inj(), val, ref, store) else val end end end defp count_ascends(parts, pi, acc) do if pi + 1 < length(parts) and Enum.at(parts, pi + 1) == @s_mt do count_ascends(parts, pi + 1, acc + 1) else {acc, pi} end end defp getpath_loop(store, parts, pi, val, has_inj, inj_key, inj_meta, src, dparent, dpath) do numparts = length(parts) if val == nil or pi >= numparts do val else raw = Enum.at(parts, pi) part0 = cond do has_inj and raw == @s_dkey -> if inj_key != nil, do: inj_key, else: raw is_binary(raw) and String.starts_with?(raw, "$GET:") -> stringify(getpath(src, slice(raw, 5, -1))) is_binary(raw) and String.starts_with?(raw, "$REF:") -> stringify(getpath(getprop(store, @s_dspec), slice(raw, 5, -1))) has_inj and is_binary(raw) and String.starts_with?(raw, "$META:") -> stringify(getpath(inj_meta, slice(raw, 6, -1))) true -> raw end part = if is_binary(part0), do: String.replace(part0, "$$", "$"), else: strkey(part0) if part == @s_mt do {ascends, pi2} = count_ascends(parts, pi, 0) if has_inj and ascends > 0 do ascends2 = if pi2 == numparts - 1, do: ascends - 1, else: ascends if ascends2 == 0 do getpath_loop(store, parts, pi2 + 1, dparent, has_inj, inj_key, inj_meta, src, dparent, dpath) else tail = Enum.drop(parts, pi2 + 1) fullpath = flatten(vlist_new([slice(dpath, -ascends2), vlist_new(tail)])) if ascends2 <= size(dpath), do: getpath(store, fullpath), else: nil end else getpath_loop(store, parts, pi2 + 1, dparent, has_inj, inj_key, inj_meta, src, dparent, dpath) end else getpath_loop(store, parts, pi + 1, getprop(val, part), has_inj, inj_key, inj_meta, src, dparent, dpath) end end end def setpath(store, path, val, injdef \\ nil) do ptype = typify(path) parts = cond do Bitwise.band(@t_list, ptype) > 0 -> vlist_new(list_items(path)) Bitwise.band(@t_string, ptype) > 0 -> vlist_new(String.split(path, ".")) Bitwise.band(@t_number, ptype) > 0 -> vlist_new([path]) true -> nil end if parts == nil do nil else base = if injdef != nil, do: idef(injdef, "base"), else: nil numparts = size(parts) parent0 = if iskey(base), do: getprop(store, base, store), else: store parent = setpath_walk(parts, numparts, parent0, 0) if is_delete(val) do delprop(parent, getelem(parts, -1)) else setprop(parent, getelem(parts, -1), val) end parent end end defp setpath_walk(parts, numparts, parent, pi) do if pi >= numparts - 1 do parent else pkey = getelem(parts, pi) np0 = getprop(parent, pkey) np = if not isnode(np0) do nextpart = getelem(parts, pi + 1) nn = if Bitwise.band(@t_number, typify(nextpart)) > 0, do: empty_list(), else: empty_map() setprop(parent, pkey, nn) nn else np0 end setpath_walk(parts, numparts, np, pi + 1) end end # --------------------------------------------------------------------------- # Injection # --------------------------------------------------------------------------- defp new_inj(val, parent) do vinj_new(%{ mode: @m_val, full: false, keyi: 0, keys: vlist_new([@s_dtop]), key: @s_dtop, ival: val, parent: parent, path: vlist_new([@s_dtop]), nodes: vlist_new([parent]), handler: &inject_handler/4, errs: empty_list(), meta: empty_map(), dparent: nil, dpath: vlist_new([@s_dtop]), base: @s_dtop, modify: nil, prior: nil, extra: nil, root: nil }) end defp inj_descend(inj) do meta = ig(inj, :meta) if ismap(meta) do d = case getprop(meta, "__d") do; n when is_number(n) -> n; _ -> 0 end setprop(meta, "__d", d + 1) end parentkey = getelem(ig(inj, :path), -2) cond do ig(inj, :dparent) == nil -> if size(ig(inj, :dpath)) > 1 do is_(inj, :dpath, vlist_new(list_items(ig(inj, :dpath)) ++ [parentkey])) end parentkey != nil -> is_(inj, :dparent, getprop(ig(inj, :dparent), parentkey)) lastpart = getelem(ig(inj, :dpath), -1) if lastpart == "$:" <> js_string(parentkey) do is_(inj, :dpath, slice(ig(inj, :dpath), -1)) else is_(inj, :dpath, vlist_new(list_items(ig(inj, :dpath)) ++ [parentkey])) end true -> :ok end ig(inj, :dparent) end defp inj_child(inj, keyi, keys) do key = strkey(getelem(keys, keyi)) val = ig(inj, :ival) vinj_new(%{ mode: ig(inj, :mode), full: ig(inj, :full), keyi: keyi, keys: keys, key: key, ival: getprop(val, key), parent: val, path: vlist_new(list_items(ig(inj, :path)) ++ [key]), nodes: vlist_new(list_items(ig(inj, :nodes)) ++ [val]), handler: ig(inj, :handler), errs: ig(inj, :errs), meta: ig(inj, :meta), base: ig(inj, :base), modify: ig(inj, :modify), prior: inj, dpath: vlist_new(list_items(ig(inj, :dpath))), dparent: ig(inj, :dparent), extra: ig(inj, :extra), root: ig(inj, :root) }) end defp inj_setval(inj, val, ancestor \\ 1) do {target, key} = if ancestor < 2 do {ig(inj, :parent), ig(inj, :key)} else {getelem(ig(inj, :nodes), -ancestor), getelem(ig(inj, :path), -ancestor)} end if val == nil, do: delprop(target, key), else: setprop(target, key, val) end # --------------------------------------------------------------------------- # inject # --------------------------------------------------------------------------- def inject(val, store, injdef \\ nil) do inj = if isinj(injdef) do injdef else parent = vmap_new([{@s_dtop, val}]) i = new_inj(val, parent) is_(i, :dparent, store) is_(i, :errs, getprop(store, @s_derrs, empty_list())) if ismap(ig(i, :meta)), do: setprop(ig(i, :meta), "__d", 0) is_(i, :root, parent) if injdef != nil do if getprop(injdef, "modify") != nil, do: is_(i, :modify, getprop(injdef, "modify")) if getprop(injdef, "extra") != nil, do: is_(i, :extra, getprop(injdef, "extra")) if getprop(injdef, "meta") != nil, do: is_(i, :meta, getprop(injdef, "meta")) if getprop(injdef, "handler") != nil, do: is_(i, :handler, getprop(injdef, "handler")) end i end inj_descend(inj) rv = cond do isnode(val) -> nodekeys = if ismap(val) do ks = Enum.map(map_pairs(val), &elem(&1, 0)) normal = ks |> Enum.filter(fn k -> not String.contains?(k, @s_ds) end) |> Enum.sort() trans = ks |> Enum.filter(fn k -> String.contains?(k, @s_ds) end) |> Enum.sort() normal ++ trans else n = length(list_items(val)) if n == 0, do: [], else: Enum.map(0..(n - 1), &Integer.to_string/1) end inject_loop(inj, val, store, nodekeys, 0) val is_binary(val) -> is_(inj, :mode, @m_val) nv = injectstr(val, store, inj) if not is_skip(nv), do: inj_setval(inj, nv) nv true -> val end if ig(inj, :modify) != nil and not is_skip(rv) do mkey = ig(inj, :key) mparent = ig(inj, :parent) mval = getprop(mparent, mkey) ig(inj, :modify).(mval, mkey, mparent, inj) end is_(inj, :ival, rv) cond do ig(inj, :prior) == nil and ig(inj, :root) != nil and haskey(ig(inj, :root), @s_dtop) -> getprop(ig(inj, :root), @s_dtop) ig(inj, :key) == @s_dtop and ig(inj, :parent) != nil and haskey(ig(inj, :parent), @s_dtop) -> getprop(ig(inj, :parent), @s_dtop) true -> rv end end defp inject_loop(inj, val, store, nodekeys, nki) do if nki >= length(nodekeys) do :ok else childinj = inj_child(inj, nki, vlist_new(nodekeys)) nodekey = ig(childinj, :key) is_(childinj, :mode, @m_keypre) prekey = injectstr(js_string(nodekey), store, childinj) nk1 = Enum.map(list_items(ig(childinj, :keys)), &js_string/1) nk2 = if prekey != nil do is_(childinj, :ival, getprop(val, prekey)) is_(childinj, :mode, @m_val) inject(ig(childinj, :ival), store, childinj) _ = Enum.map(list_items(ig(childinj, :keys)), &js_string/1) is_(childinj, :mode, @m_keypost) injectstr(js_string(nodekey), store, childinj) Enum.map(list_items(ig(childinj, :keys)), &js_string/1) else nk1 end inject_loop(inj, val, store, nk2, ig(childinj, :keyi) + 1) end end defp inject_handler(inj, val, ref, store) do iscmd = isfunc(val) and (ref == nil or (is_binary(ref) and String.starts_with?(ref, @s_ds))) cond do iscmd -> val.(inj, val, ref, store) ig(inj, :mode) == @m_val and ig(inj, :full) -> inj_setval(inj, val) val true -> val end end defp injectstr(val, store, inj) do if val == @s_mt do @s_mt else case Regex.run(@r_inject_full, val) do [_, pathref0] -> if inj != nil, do: is_(inj, :full, true) pathref = if String.length(pathref0) > 3, do: pathref0 |> String.replace("$BT", @s_bt) |> String.replace("$DS", @s_ds), else: pathref0 getpath(store, pathref, inj) _ -> out = Regex.replace(@r_inject_part, val, fn _whole, ref0 -> ref = if String.length(ref0) > 3, do: ref0 |> String.replace("$BT", @s_bt) |> String.replace("$DS", @s_ds), else: ref0 if inj != nil, do: is_(inj, :full, false) found = getpath(store, ref, inj) cond do found == nil -> @s_mt is_binary(found) -> if found == "__NULL__", do: "null", else: found isfunc(found) -> @s_mt true -> try do; json_encode(found); rescue _ -> stringify(found) end end end) if inj != nil and isfunc(ig(inj, :handler)) do is_(inj, :full, true) ig(inj, :handler).(inj, out, val, store) else out end end end end # --------------------------------------------------------------------------- # transform commands # --------------------------------------------------------------------------- defp transform_delete(inj, _v, _r, _s) do delprop(ig(inj, :parent), ig(inj, :key)) nil end defp transform_copy(inj, _v, _r, _s) do if ig(inj, :mode) == @m_keypre or ig(inj, :mode) == @m_keypost do ig(inj, :key) else out = lookup_(ig(inj, :dparent), ig(inj, :key)) inj_setval(inj, out) out end end defp transform_key(inj, _v, _r, _s) do if ig(inj, :mode) != @m_val do nil else keyspec = lookup_(ig(inj, :parent), @s_bkey) if keyspec != nil do delprop(ig(inj, :parent), @s_bkey) getprop(ig(inj, :dparent), keyspec) else anno = lookup_(ig(inj, :parent), @s_banno) fromanno = lookup_(anno, @s_key) if fromanno != nil, do: fromanno, else: getelem(ig(inj, :path), -2) end end end defp transform_anno(inj, _v, _r, _s) do delprop(ig(inj, :parent), @s_banno) nil end defp transform_merge(inj, _v, _r, _s) do cond do ig(inj, :mode) == @m_keypre -> ig(inj, :key) ig(inj, :mode) == @m_keypost -> args0 = getprop(ig(inj, :parent), ig(inj, :key)) args = if islist(args0), do: args0, else: vlist_new([args0]) inj_setval(inj, nil) mergelist = flatten(vlist_new([vlist_new([ig(inj, :parent)]), args, vlist_new([clone(ig(inj, :parent))])])) merge(mergelist) ig(inj, :key) true -> nil end end defp transform_each(inj, _v, _r, store) do if islist(ig(inj, :keys)), do: slice(ig(inj, :keys), 0, 1, true) if ig(inj, :mode) != @m_val do nil else parent = ig(inj, :parent) srcpath = if size(parent) > 1, do: getelem(parent, 1), else: nil child_tm = if size(parent) > 2, do: clone(getelem(parent, 2)), else: nil srcstore = getprop(store, ig(inj, :base), store) src = getpath(srcstore, srcpath, inj) tkey = getelem(ig(inj, :path), -2) nodes = ig(inj, :nodes) target = (fn -> t = getelem(nodes, -2); if t == nil, do: getelem(nodes, -1), else: t end).() rval = vlist_new([]) if isnode(src) do tval_items = cond do islist(src) -> Enum.map(list_items(src), fn _ -> clone(child_tm) end) ismap(src) -> Enum.map(map_pairs(src), fn {k, _} -> cc = clone(child_tm) if ismap(cc), do: setprop(cc, @s_banno, vmap_new([{@s_key, k}])) cc end) true -> [] end tval = vlist_new(tval_items) tcurrent = cond do ismap(src) -> vlist_new(Enum.map(map_pairs(src), &elem(&1, 1))) islist(src) -> vlist_new(list_items(src)) true -> src end if tval_items != [] do path = ig(inj, :path) ckey = getelem(path, -2) plist = list_items(path) tpath = if plist == [], do: vlist_new([]), else: vlist_new(Enum.take(plist, length(plist) - 1)) dpath0 = [@s_dtop] dpath0 = if is_binary(srcpath) and srcpath != @s_mt do dpath0 ++ (String.split(srcpath, ".") |> Enum.filter(&(&1 != @s_mt))) else dpath0 end dpath0 = if ckey != nil, do: dpath0 ++ ["$:" <> js_string(ckey)], else: dpath0 tcur = vmap_new([{js_string(ckey), tcurrent}]) {tcur, dpath0} = if size(tpath) > 1 do pkey = getelem(path, -3, @s_dtop) {vmap_new([{js_string(pkey), tcur}]), dpath0 ++ ["$:" <> js_string(pkey)]} else {tcur, dpath0} end tinj = inj_child(inj, 0, if(ckey != nil, do: vlist_new([ckey]), else: vlist_new([]))) is_(tinj, :path, tpath) nlist = list_items(nodes) is_(tinj, :nodes, if(nlist == [], do: vlist_new([]), else: vlist_new(Enum.take(nlist, length(nlist) - 1)))) is_(tinj, :parent, if(size(ig(tinj, :nodes)) > 0, do: getelem(ig(tinj, :nodes), -1), else: nil)) if ckey != nil and ig(tinj, :parent) != nil, do: setprop(ig(tinj, :parent), ckey, tval) is_(tinj, :ival, tval) is_(tinj, :dpath, vlist_new(dpath0)) is_(tinj, :dparent, tcur) inject(tval, store, tinj) rval = ig(tinj, :ival) setprop(target, tkey, rval) if islist(rval) and size(rval) > 0, do: getelem(rval, 0), else: nil else setprop(target, tkey, rval) if islist(rval) and size(rval) > 0, do: getelem(rval, 0), else: nil end else setprop(target, tkey, rval) nil end end end defp transform_pack(inj, _v, _r, store) do cond do ig(inj, :mode) != @m_keypre or not is_binary(ig(inj, :key)) -> nil true -> parent = ig(inj, :parent) path = ig(inj, :path) nodes = ig(inj, :nodes) args_val = getprop(parent, ig(inj, :key)) if not islist(args_val) or size(args_val) < 2 do nil else srcpath = getelem(args_val, 0) origchildspec = getelem(args_val, 1) tkey = getelem(path, -2) pathsize = size(path) target = (fn -> t = getelem(nodes, pathsize - 2); if t == nil, do: getelem(nodes, pathsize - 1), else: t end).() srcstore = getprop(store, ig(inj, :base), store) src0 = getpath(srcstore, srcpath, inj) src = if not islist(src0) do if ismap(src0) do vlist_new( Enum.map(items_pairs(src0), fn {k, node} -> setprop(node, @s_banno, vmap_new([{@s_key, k}])) node end) ) else nil end else src0 end if src == nil do nil else keypath = getprop(origchildspec, @s_bkey) childspec = delprop(origchildspec, @s_bkey) child = getprop(childspec, @s_bval, childspec) tval = empty_map() Enum.each(items_pairs(src), fn {srckey, srcnode} -> k = cond do keypath == nil -> srckey is_binary(keypath) and String.starts_with?(keypath, @s_bt) -> inject(keypath, merge(vlist_new([empty_map(), store, vmap_new([{@s_dtop, srcnode}])]), 1)) true -> getpath(srcnode, keypath, inj) end tchild = clone(child) setprop(tval, k, tchild) anno = getprop(srcnode, @s_banno) if anno == nil, do: delprop(tchild, @s_banno), else: setprop(tchild, @s_banno, anno) end) rval = if not isempty(tval) do tsrc = empty_map() list_items(src) |> Enum.with_index() |> Enum.each(fn {node, i} -> kn = cond do keypath == nil -> i is_binary(keypath) and String.starts_with?(keypath, @s_bt) -> inject(keypath, merge(vlist_new([empty_map(), store, vmap_new([{@s_dtop, node}])]), 1)) true -> getpath(node, keypath, inj) end setprop(tsrc, kn, node) end) tpath = slice(ig(inj, :path), -1) ckey = getelem(ig(inj, :path), -2) dpath = flatten(vlist_new([@s_dtop, vlist_new(String.split(srcpath, ".")), "$:" <> js_string(ckey)])) tcur = if size(tpath) > 1 do pkey = getelem(ig(inj, :path), -3, @s_dtop) setprop(dpath, size(dpath), "$:" <> js_string(pkey)) vmap_new([{js_string(pkey), vmap_new([{js_string(ckey), tsrc}])}]) else vmap_new([{js_string(ckey), tsrc}]) end tinj = inj_child(inj, 0, vlist_new([ckey])) is_(tinj, :path, tpath) is_(tinj, :nodes, slice(ig(inj, :nodes), -1)) is_(tinj, :parent, getelem(ig(tinj, :nodes), -1)) is_(tinj, :ival, tval) is_(tinj, :dpath, dpath) is_(tinj, :dparent, tcur) inject(tval, store, tinj) ig(tinj, :ival) else empty_map() end setprop(target, tkey, rval) nil end end end end defp transform_ref(inj, val, _r, store) do if ig(inj, :mode) != @m_val do nil else nodes = ig(inj, :nodes) refpath = lookup_(ig(inj, :parent), 1) is_(inj, :keyi, size(ig(inj, :keys))) spec_func = getprop(store, @s_dspec) if not isfunc(spec_func) do nil else spec = spec_func.(inj, nil, "", store) refv = getpath(spec, refpath) flag = vlist_new([false]) if isnode(refv) do walk(refv, after: fn _k, v2, _p, _path -> if v2 == "`$REF`", do: setprop(flag, 0, true) v2 end) end has_sub = getelem(flag, 0) == true tref = clone(refv) cpath = slice(ig(inj, :path), 0, size(ig(inj, :path)) - 3) tpath = slice(ig(inj, :path), 0, size(ig(inj, :path)) - 1) tcur = getpath(store, cpath) tval = getpath(store, tpath) rval = if refv != nil and (not has_sub or tval != nil) do cs = inj_child(inj, 0, vlist_new([getelem(tpath, -1)])) is_(cs, :path, tpath) is_(cs, :nodes, slice(ig(inj, :nodes), 0, size(ig(inj, :nodes)) - 1)) is_(cs, :parent, getelem(nodes, -2)) is_(cs, :ival, tref) is_(cs, :dparent, tcur) inject(tref, store, cs) ig(cs, :ival) else nil end inj_setval(inj, rval, 2) if islist(ig(inj, :parent)) and ig(inj, :prior) != nil do is_(ig(inj, :prior), :keyi, ig(ig(inj, :prior), :keyi) - 1) end val end end end defp formatter_tbl do %{ "identity" => fn _k, v -> v end, "upper" => fn _k, v -> if isnode(v), do: v, else: String.upcase(js_string(v)) end, "lower" => fn _k, v -> if isnode(v), do: v, else: String.downcase(js_string(v)) end, "string" => fn _k, v -> if isnode(v), do: v, else: js_string(v) end, "number" => fn _k, v -> if isnode(v) do v else n = parse_num(js_string(v)) if is_intish(n), do: trunc(n), else: n end end, "integer" => fn _k, v -> if isnode(v), do: v, else: trunc(parse_num(js_string(v))) end, "concat" => fn k, v -> if k == nil and islist(v) do join(items_v(v, fn {_, x} -> if isnode(x), do: @s_mt, else: js_string(x) end), @s_mt) else v end end } end defp parse_num(s) do case Float.parse(s) do {n, _} -> n :error -> 0 end end def check_placement(modes, ijname, parent_types, inj) do modenum = mode_to_num(ig(inj, :mode)) cond do Bitwise.band(modes, modenum) == 0 -> allowed = Enum.filter([1, 2, 4], fn m -> Bitwise.band(modes, m) != 0 end) placements = allowed |> Enum.map(fn m -> if m == 4, do: "value", else: "key" end) |> Enum.join(",") cur = if modenum == 4, do: "value", else: "key" push_err(inj, "$" <> ijname <> ": invalid placement as " <> cur <> ", expected: " <> placements <> ".") false not isempty(parent_types) -> ptype = typify(ig(inj, :parent)) if Bitwise.band(parent_types, ptype) == 0 do push_err(inj, "$" <> ijname <> ": invalid placement in parent " <> typename(ptype) <> ", expected: " <> typename(parent_types) <> ".") false else true end true -> true end end def injector_args(arg_types, args) do numargs = length(arg_types) found = injector_args_loop(arg_types, args, 0, numargs, List.duplicate(nil, 1 + numargs)) vlist_new(found) end defp injector_args_loop(arg_types, args, argi, numargs, found) do if argi >= numargs do found else arg = getelem(args, argi) arg_type = typify(arg) at = Enum.at(arg_types, argi) if Bitwise.band(at, arg_type) == 0 do List.replace_at(found, 0, "invalid argument: " <> stringify(arg, 22) <> " (" <> typename(arg_type) <> " at position " <> Integer.to_string(1 + argi) <> ") is not of type: " <> typename(at) <> ".") else injector_args_loop(arg_types, args, argi + 1, numargs, List.replace_at(found, 1 + argi, arg)) end end end def inject_child(child, store, inj) do cinj = if ig(inj, :prior) != nil do prior = ig(inj, :prior) if ig(prior, :prior) != nil do c = inj_child(ig(prior, :prior), ig(prior, :keyi), ig(prior, :keys)) is_(c, :ival, child) setprop(ig(c, :parent), ig(prior, :key), child) c else c = inj_child(prior, ig(inj, :keyi), ig(inj, :keys)) is_(c, :ival, child) setprop(ig(c, :parent), ig(inj, :key), child) c end else inj end inject(child, store, cinj) cinj end defp transform_format(inj, _v, _r, store) do slice(ig(inj, :keys), 0, 1, true) if ig(inj, :mode) != @m_val do nil else name = lookup_(ig(inj, :parent), 1) child = lookup_(ig(inj, :parent), 2) tkey = getelem(ig(inj, :path), -2) target = (fn -> t = getelem(ig(inj, :nodes), -2); if t == nil, do: getelem(ig(inj, :nodes), -1), else: t end).() cinj = inject_child(child, store, inj) resolved = ig(cinj, :ival) formatter = if Bitwise.band(@t_function, typify(name)) > 0 do fn k, v -> name.(dummy_inj(), v, js_string(k), nil) end else Map.get(formatter_tbl(), js_string(name)) end if formatter == nil do push_err(inj, "$FORMAT: unknown format: " <> js_string(name) <> ".") nil else out = walk(resolved, after: fn k, v, _p, _path -> formatter.(k, v) end) setprop(target, tkey, out) out end end end defp transform_apply(inj, _v, _r, store) do if not check_placement(4, "APPLY", @t_list, inj) do nil else res = injector_args([@t_function, @t_any], slice(ig(inj, :parent), 1)) err = getelem(res, 0) apply_fn = getelem(res, 1) child = if size(res) > 2, do: getelem(res, 2), else: nil if err != nil do push_err(inj, "$APPLY: " <> js_string(err)) nil else tkey = getelem(ig(inj, :path), -2) target = (fn -> t = getelem(ig(inj, :nodes), -2); if t == nil, do: getelem(ig(inj, :nodes), -1), else: t end).() cinj = inject_child(child, store, inj) resolved = ig(cinj, :ival) out = apply_fn.(resolved, store, cinj) setprop(target, tkey, out) out end end end def transform(data, spec0, injdef \\ nil) do origspec = spec0 spec = clone(spec0) extra = if injdef != nil, do: getprop(injdef, "extra"), else: nil collect = injdef != nil and getprop(injdef, "errs") != nil errs = if collect, do: getprop(injdef, "errs"), else: empty_list() extra_transforms = empty_map() extra_data = empty_map() if extra != nil do Enum.each(items_pairs(extra), fn {k, v} -> if String.starts_with?(k, @s_ds), do: setprop(extra_transforms, k, v), else: setprop(extra_data, k, v) end) end data_clone = merge(vlist_new([if(isempty(extra_data), do: nil, else: clone(extra_data)), clone(data)])) store = empty_map() setprop(store, @s_dtop, data_clone) setprop(store, @s_dspec, fn _i, _v, _r, _s -> origspec end) setprop(store, "$BT", fn _i, _v, _r, _s -> @s_bt end) setprop(store, "$DS", fn _i, _v, _r, _s -> @s_ds end) setprop(store, "$WHEN", fn _i, _v, _r, _s -> "1970-01-01T00:00:00.000Z" end) setprop(store, "$DELETE", &transform_delete/4) setprop(store, "$COPY", &transform_copy/4) setprop(store, "$KEY", &transform_key/4) setprop(store, "$ANNO", &transform_anno/4) setprop(store, "$MERGE", &transform_merge/4) setprop(store, "$EACH", &transform_each/4) setprop(store, "$PACK", &transform_pack/4) setprop(store, "$REF", &transform_ref/4) setprop(store, "$FORMAT", &transform_format/4) setprop(store, "$APPLY", &transform_apply/4) Enum.each(items_pairs(extra_transforms), fn {k, v} -> setprop(store, k, v) end) setprop(store, @s_derrs, errs) idef = empty_map() if ismap(injdef), do: Enum.each(items_pairs(injdef), fn {k, v} -> setprop(idef, k, v) end) setprop(idef, "errs", errs) out = inject(spec, store, idef) if size(errs) > 0 and not collect, do: raise(Voxgig.Struct.Error, message: join(errs, " | ")) out end # --------------------------------------------------------------------------- # validate # --------------------------------------------------------------------------- defp push_err(inj, msg), do: setprop(ig(inj, :errs), size(ig(inj, :errs)), msg) defp invalid_type_msg(path, needtype, vt, v, _whence) do vs = if v == nil, do: "no value", else: stringify(v) "Expected " <> (if size(path) > 1, do: "field " <> pathify(path, 1) <> " to be ", else: "") <> needtype <> ", but found " <> (if v != nil, do: typename(vt) <> @s_viz, else: "") <> vs <> "." end defp validate_string(inj, _v, _r, _s) do out = lookup_(ig(inj, :dparent), ig(inj, :key)) t = typify(out) cond do Bitwise.band(@t_string, t) == 0 -> push_err(inj, invalid_type_msg(ig(inj, :path), "string", t, out, "V1010")); nil out == @s_mt -> push_err(inj, "Empty string at " <> pathify(ig(inj, :path), 1)); nil true -> out end end defp validate_type(inj, _v, ref, _s) do tname = if is_binary(ref) and String.length(ref) > 1, do: String.downcase(String.slice(ref, 1, String.length(ref) - 1)), else: "any" idx = type_index(tname) typev0 = if idx >= 0, do: Bitwise.bsl(1, 31 - idx), else: 0 typev = if tname == "nil", do: Bitwise.bor(typev0, @t_null), else: typev0 out = lookup_(ig(inj, :dparent), ig(inj, :key)) t = typify(out) if Bitwise.band(t, typev) == 0 do push_err(inj, invalid_type_msg(ig(inj, :path), tname, t, out, "V1001")) nil else out end end defp type_index(tname) do Enum.find_index(Tuple.to_list(@typename), fn x -> x == tname end) || -1 end defp validate_any(inj, _v, _r, _s), do: lookup_(ig(inj, :dparent), ig(inj, :key)) defp validate_child(inj, _v, _r, _s) do parent = ig(inj, :parent) key = ig(inj, :key) path = ig(inj, :path) keys = ig(inj, :keys) cond do ig(inj, :mode) == @m_keypre -> childtm = getprop(parent, key) pkey = getelem(path, -2) tval = getprop(ig(inj, :dparent), pkey) cond do tval == nil -> Enum.each(keysof(empty_map()), fn ckey -> setprop(parent, ckey, clone(childtm)); setprop(keys, size(keys), ckey) end) delprop(parent, key) nil not ismap(tval) -> push_err(inj, invalid_type_msg(slice(path, 0, size(path) - 1), "object", typify(tval), tval, "V0220")) nil true -> Enum.each(keysof(tval), fn ckey -> setprop(parent, ckey, clone(childtm)); setprop(keys, size(keys), ckey) end) delprop(parent, key) nil end ig(inj, :mode) == @m_val -> childtm = getprop(parent, 1) cond do not islist(parent) -> push_err(inj, "Invalid $CHILD as value") nil ig(inj, :dparent) == nil -> list_set_items(parent, []) nil not islist(ig(inj, :dparent)) -> push_err(inj, invalid_type_msg(slice(path, 0, size(path) - 1), "list", typify(ig(inj, :dparent)), ig(inj, :dparent), "V0230")) is_(inj, :keyi, size(parent)) ig(inj, :dparent) true -> Enum.each(items_pairs(ig(inj, :dparent)), fn {k, _} -> setprop(parent, k, clone(childtm)) end) n = size(ig(inj, :dparent)) list_set_items(parent, Enum.take(list_items(parent), n)) is_(inj, :keyi, 0) getprop(ig(inj, :dparent), 0) end true -> nil end end defp validate_one(inj, _v, _r, store) do if ig(inj, :mode) != @m_val do nil else parent = ig(inj, :parent) if not islist(parent) or ig(inj, :keyi) != 0 do push_err(inj, "The $ONE validator at field " <> pathify(ig(inj, :path), 1, 1) <> " must be the first element of an array.") nil else is_(inj, :keyi, size(ig(inj, :keys))) inj_setval(inj, ig(inj, :dparent), 2) is_(inj, :path, slice(ig(inj, :path), 0, size(ig(inj, :path)) - 1)) is_(inj, :key, getelem(ig(inj, :path), -1)) tvals = slice(parent, 1) if size(tvals) == 0 do push_err(inj, "The $ONE validator at field " <> pathify(ig(inj, :path), 1, 1) <> " must have at least one argument.") nil else matched = validate_one_loop(inj, store, list_items(tvals), false) if not matched do valdesc = Enum.map_join(list_items(tvals), ", ", &stringify/1) valdesc = Regex.replace(@r_transform_name, valdesc, fn _w, g1 -> String.downcase(g1) end) push_err(inj, invalid_type_msg(ig(inj, :path), (if size(tvals) > 1, do: "one of ", else: "") <> valdesc, typify(ig(inj, :dparent)), ig(inj, :dparent), "V0210")) end nil end end end end defp validate_one_loop(_inj, _store, [], matched), do: matched defp validate_one_loop(inj, store, [tv | rest], matched) do if matched do true else terrs = empty_list() vstore = merge(vlist_new([empty_map(), store]), 1) setprop(vstore, @s_dtop, ig(inj, :dparent)) idef = vmap_new([{"extra", vstore}, {"errs", terrs}, {"meta", ig(inj, :meta)}]) vcurrent = validate(ig(inj, :dparent), tv, idef) inj_setval(inj, vcurrent, -2) if size(terrs) == 0, do: true, else: validate_one_loop(inj, store, rest, false) end end defp validate_exact(inj, _v, _r, _s) do if ig(inj, :mode) != @m_val do delprop(ig(inj, :parent), ig(inj, :key)) nil else parent = ig(inj, :parent) if not islist(parent) or ig(inj, :keyi) != 0 do push_err(inj, "The $EXACT validator at field " <> pathify(ig(inj, :path), 1, 1) <> " must be the first element of an array.") nil else is_(inj, :keyi, size(ig(inj, :keys))) inj_setval(inj, ig(inj, :dparent), 2) is_(inj, :path, slice(ig(inj, :path), 0, size(ig(inj, :path)) - 1)) is_(inj, :key, getelem(ig(inj, :path), -1)) tvals = slice(parent, 1) if size(tvals) == 0 do push_err(inj, "The $EXACT validator at field " <> pathify(ig(inj, :path), 1, 1) <> " must have at least one argument.") nil else matched = Enum.any?(list_items(tvals), fn tv -> veq(tv, ig(inj, :dparent)) end) if not matched do valdesc = Enum.map_join(list_items(tvals), ", ", &stringify/1) valdesc = Regex.replace(@r_transform_name, valdesc, fn _w, g1 -> String.downcase(g1) end) push_err(inj, invalid_type_msg(ig(inj, :path), (if size(ig(inj, :path)) > 1, do: "", else: "value ") <> "exactly equal to " <> (if size(tvals) == 1, do: "", else: "one of ") <> valdesc, typify(ig(inj, :dparent)), ig(inj, :dparent), "V0110")) end nil end end end end def veq(a, b) do cond do a == nil and b == nil -> true is_boolean(a) or is_boolean(b) -> a == b is_number(a) and is_number(b) -> a == b is_binary(a) and is_binary(b) -> a == b islist(a) and islist(b) -> ia = list_items(a); ib = list_items(b); length(ia) == length(ib) and Enum.all?(Enum.zip(ia, ib), fn {x, y} -> veq(x, y) end) ismap(a) and ismap(b) -> veq_map(a, b) true -> a == b end end defp veq_map(a, b) do pa = map_pairs(a) pb = map_pairs(b) length(pa) == length(pb) and Enum.all?(pa, fn {k, v} -> case omap_get(pb, k) do; {:ok, w} -> veq(v, w); :error -> false end end) end defp validation(pval, key, parent, inj) do if is_skip(pval) do :ok else exact = getprop(ig(inj, :meta), @s_bexact, false) cval = getprop(ig(inj, :dparent), key) exact_b = exact == true if not exact_b and cval == nil do :ok else ptype = typify(pval) if Bitwise.band(@t_string, ptype) > 0 and String.contains?(js_string(pval), @s_ds) do :ok else ctype = typify(cval) cond do ptype != ctype and pval != nil -> push_err(inj, invalid_type_msg(ig(inj, :path), typename(ptype), ctype, cval, "V0010")) ismap(cval) -> if not ismap(pval) do push_err(inj, invalid_type_msg(ig(inj, :path), typename(ptype), ctype, cval, "V0020")) else ckeys = keysof(cval) pkeys = keysof(pval) if pkeys != [] and getprop(pval, @s_bopen) != true do badkeys = Enum.filter(ckeys, fn ck -> lookup_(pval, ck) == nil end) if badkeys != [], do: push_err(inj, "Unexpected keys at field " <> pathify(ig(inj, :path), 1) <> @s_viz <> Enum.join(badkeys, ", ")) else merge(vlist_new([pval, cval])) if isnode(pval), do: delprop(pval, @s_bopen) end end islist(cval) -> if not islist(pval), do: push_err(inj, invalid_type_msg(ig(inj, :path), typename(ptype), ctype, cval, "V0030")) exact_b -> if not veq(cval, pval) do pathmsg = if size(ig(inj, :path)) > 1, do: "at field " <> pathify(ig(inj, :path), 1) <> ": ", else: "" push_err(inj, "Value " <> pathmsg <> js_string(cval) <> " should equal " <> js_string(pval) <> ".") end true -> setprop(parent, key, cval) end :ok end end end end defp validate_handler(inj, val, ref, store) do m = if is_binary(ref), do: Regex.run(@r_meta_path, ref), else: nil case m do [_, _, g2, _] -> if g2 == "=", do: inj_setval(inj, vlist_new([@s_bexact, val])), else: inj_setval(inj, val) is_(inj, :keyi, -1) @skip _ -> inject_handler(inj, val, ref, store) end end def validate(data, spec, injdef \\ nil) do extra = getprop(injdef, "extra") collect = injdef != nil and getprop(injdef, "errs") != nil errs = if collect, do: getprop(injdef, "errs"), else: empty_list() base = empty_map() Enum.each(["$DELETE", "$COPY", "$KEY", "$META", "$MERGE", "$EACH", "$PACK"], fn k -> setprop(base, k, nil) end) setprop(base, "$STRING", &validate_string/4) Enum.each(["$NUMBER", "$INTEGER", "$DECIMAL", "$BOOLEAN", "$NULL", "$NIL", "$MAP", "$LIST", "$FUNCTION", "$INSTANCE"], fn k -> setprop(base, k, &validate_type/4) end) setprop(base, "$ANY", &validate_any/4) setprop(base, "$CHILD", &validate_child/4) setprop(base, "$ONE", &validate_one/4) setprop(base, "$EXACT", &validate_exact/4) store = merge(vlist_new([base, if(extra == nil, do: empty_map(), else: extra), vmap_new([{@s_derrs, errs}])]), 1) meta = getprop(injdef, "meta", empty_map()) setprop(meta, @s_bexact, getprop(meta, @s_bexact, false)) idef = vmap_new([{"meta", meta}, {"extra", store}, {"modify", &validation/4}, {"handler", &validate_handler/4}, {"errs", errs}]) out = transform(data, spec, idef) if size(errs) > 0 and not collect, do: raise(Voxgig.Struct.Error, message: join(errs, " | ")) out end # --------------------------------------------------------------------------- # select # --------------------------------------------------------------------------- defp select_and(inj, _v, _r, store) do if ig(inj, :mode) == @m_keypre do terms = getprop(ig(inj, :parent), ig(inj, :key)) ppath = slice(ig(inj, :path), -1) point = getpath(store, ppath) vstore = merge(vlist_new([empty_map(), store]), 1) setprop(vstore, @s_dtop, point) Enum.each(items_pairs(terms), fn {_, term} -> terrs = empty_list() validate(point, term, vmap_new([{"extra", vstore}, {"errs", terrs}, {"meta", ig(inj, :meta)}])) if size(terrs) != 0, do: push_err(inj, "AND:" <> pathify(ppath) <> "⨯" <> stringify(point) <> " fail:" <> stringify(terms)) end) gkey = getelem(ig(inj, :path), -2) gp = getelem(ig(inj, :nodes), -2) setprop(gp, gkey, point) end nil end defp select_or(inj, _v, _r, store) do if ig(inj, :mode) == @m_keypre do terms = getprop(ig(inj, :parent), ig(inj, :key)) ppath = slice(ig(inj, :path), -1) point = getpath(store, ppath) vstore = merge(vlist_new([empty_map(), store]), 1) setprop(vstore, @s_dtop, point) done = select_or_loop(inj, store, vstore, ppath, point, items_pairs(terms)) if not done, do: push_err(inj, "OR:" <> pathify(ppath) <> "⨯" <> stringify(point) <> " fail:" <> stringify(terms)) end nil end defp select_or_loop(_inj, _store, _vstore, _ppath, _point, []), do: false defp select_or_loop(inj, store, vstore, ppath, point, [{_, term} | rest]) do terrs = empty_list() validate(point, term, vmap_new([{"extra", vstore}, {"errs", terrs}, {"meta", ig(inj, :meta)}])) if size(terrs) == 0 do gkey = getelem(ig(inj, :path), -2) gp = getelem(ig(inj, :nodes), -2) setprop(gp, gkey, point) true else select_or_loop(inj, store, vstore, ppath, point, rest) end end defp select_not(inj, _v, _r, store) do if ig(inj, :mode) == @m_keypre do term = getprop(ig(inj, :parent), ig(inj, :key)) ppath = slice(ig(inj, :path), -1) point = getpath(store, ppath) vstore = merge(vlist_new([empty_map(), store]), 1) setprop(vstore, @s_dtop, point) terrs = empty_list() validate(point, term, vmap_new([{"extra", vstore}, {"errs", terrs}, {"meta", ig(inj, :meta)}])) if size(terrs) == 0, do: push_err(inj, "NOT:" <> pathify(ppath) <> "⨯" <> stringify(point) <> " fail:" <> stringify(term)) gkey = getelem(ig(inj, :path), -2) gp = getelem(ig(inj, :nodes), -2) setprop(gp, gkey, point) end nil end defp num_cmp(a, b, op) do if is_number(a) and is_number(b) do case op do :gt -> a > b :lt -> a < b :gte -> a >= b :lte -> a <= b end else false end end defp select_cmp(inj, _v, ref, store) do if ig(inj, :mode) == @m_keypre do term = getprop(ig(inj, :parent), ig(inj, :key)) gkey = getelem(ig(inj, :path), -2) ppath = slice(ig(inj, :path), -1) point = getpath(store, ppath) pass = cond do ref == "$GT" -> num_cmp(point, term, :gt) ref == "$LT" -> num_cmp(point, term, :lt) ref == "$GTE" -> num_cmp(point, term, :gte) ref == "$LTE" -> num_cmp(point, term, :lte) ref == "$LIKE" -> is_binary(term) and Regex.match?(Regex.compile!(term), stringify(point)) true -> false end if pass do gp = getelem(ig(inj, :nodes), -2) setprop(gp, gkey, point) else push_err(inj, "CMP: " <> pathify(ppath) <> "⨯" <> stringify(point) <> " fail:" <> js_string(ref) <> " " <> stringify(term)) end end nil end def select(children0, query) do if not isnode(children0) do empty_list() else children = if ismap(children0) do vlist_new(Enum.map(items_pairs(children0), fn {k, n} -> setprop(n, @s_dkey, k); n end)) else vlist_new( list_items(children0) |> Enum.with_index() |> Enum.map(fn {n, i} -> if ismap(n), do: (setprop(n, @s_dkey, i); n), else: n end) ) end results = empty_list() extra = empty_map() setprop(extra, "$AND", &select_and/4) setprop(extra, "$OR", &select_or/4) setprop(extra, "$NOT", &select_not/4) setprop(extra, "$GT", &select_cmp/4) setprop(extra, "$LT", &select_cmp/4) setprop(extra, "$GTE", &select_cmp/4) setprop(extra, "$LTE", &select_cmp/4) setprop(extra, "$LIKE", &select_cmp/4) q = clone(query) walk(q, after: fn _k, v, _p, _path -> if ismap(v), do: setprop(v, @s_bopen, getprop(v, @s_bopen, true)) v end) Enum.each(list_items(children), fn child -> errs = empty_list() meta = empty_map() setprop(meta, @s_bexact, true) idef = vmap_new([{"errs", errs}, {"meta", meta}, {"extra", extra}]) validate(child, clone(q), idef) if size(errs) == 0, do: setprop(results, size(results), child) end) results end end # --------------------------------------------------------------------------- # builders # --------------------------------------------------------------------------- def jm(kv) do m = empty_map() n = length(kv) jm_loop(m, kv, n, 0) m end defp jm_loop(m, kv, n, i) do if i >= n do m else k0 = Enum.at(kv, i) k = cond do; k0 == nil -> "null"; is_binary(k0) -> k0; true -> stringify(k0) end setprop(m, k, if(i + 1 < n, do: Enum.at(kv, i + 1), else: nil)) jm_loop(m, kv, n, i + 2) end end def jt(v), do: vlist_new(v) def tn(t), do: typename(t) end defmodule Voxgig.Struct.Error do defexception message: "struct error" end