defmodule Eflatbuffers.Schema do @referenced_types [ :string, :byte, :ubyte, :bool, :short, :ushort, :int, :uint, :float, :long, :ulong, :double, ] def parse!(schema_str) do case parse(schema_str) do {:ok, schema} -> schema {:error, error} -> throw({:error, error}) end end def parse(schema_str) when is_binary(schema_str) do tokens = lexer(schema_str) case :schema_parser.parse(tokens) do {:ok, data} -> {:ok, decorate(data)} error -> error end end def lexer(schema_str) do {:ok, tokens, _} = to_charlist(schema_str) |> :schema_lexer.string tokens end # this preprocesses the schema # in order to keep the read/write # code as simple as possible # correlate tables with names # and define defaults explicitly def decorate({entities, options}) do entities_decorated = Enum.reduce( entities, %{}, # for a tables we transform # the types to explicitly signify # vectors, tables, and enums fn({key, {:table, fields}}, acc) -> Map.put( acc, key, { :table, table_options(fields, entities) } ) # for enums we change the list of options # into a map for faster lookup when # writing and reading ({key, {{:enum, type}, fields}}, acc) -> hash = Enum.reduce( Enum.with_index(fields), %{}, fn({field, index}, hash_acc) -> Map.put(hash_acc, field, index) |> Map.put(index, field) end ) Map.put(acc, key, {:enum, %{ type: {type, %{ default: 0 }}, members: hash }}) ({key, {:union, fields}}, acc) -> hash = Enum.reduce( Enum.with_index(fields), %{}, fn({field, index}, hash_acc) -> Map.put(hash_acc, field, index) |> Map.put(index, field) end ) Map.put(acc, key, {:union, %{ members: hash }}) end ) {entities_decorated, options} end def table_options(fields, entities) do fields_and_indices(fields, entities, {0, [], %{}}) end def fields_and_indices([], _, {_, fields, indices}) do %{ fields: Enum.reverse(fields), indices: indices } end def fields_and_indices([{field_name, field_value} | fields], entities, {index, fields_acc, indices_acc}) do index_offset = index_offset(field_value, entities) decorated_type = decorate_field(field_value, entities) index_new = index + index_offset fields_acc_new = [{field_name, decorated_type} | fields_acc] indices_acc_new = Map.put(indices_acc, field_name, {index, decorated_type}) fields_and_indices(fields, entities, {index_new, fields_acc_new, indices_acc_new}) end def index_offset(field_value, entities) do case is_referenced?(field_value) do true -> case Map.get(entities, field_value) do {:union, _} -> 2 _ -> 1 end false -> 1 end end def decorate_field({:vector, type}, entities) do {:vector, %{ type: decorate_field(type, entities) }} end def decorate_field(field_value, entities) do case is_referenced?(field_value) do true -> decorate_referenced_field(field_value, entities) false -> decorate_field(field_value) end end def decorate_referenced_field(field_value, entities) do case Map.get(entities, field_value) do nil -> throw({:error, {:entity_not_found, field_value}}) {:table, _} -> {:table, %{ name: field_value }} {{:enum, _}, _} -> {:enum, %{ name: field_value }} {:union, _} -> {:union, %{ name: field_value }} end end def decorate_field({type, default}) do {type, %{ default: default} } end def decorate_field(:bool) do {:bool, %{ default: false }} end def decorate_field(:string) do {:string, %{}} end def decorate_field(type) do {type, %{ default: 0 }} end def is_referenced?({type, _default}) do is_referenced?(type) end def is_referenced?(type) do not Enum.member?(@referenced_types, type) end end