diff --git a/.travis.yml b/.travis.yml index 27b5e17..4b1e8ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ otp_release: - 17.4 - 18.2.1 elixir: - - 1.1.1 - 1.2.3 - 1.3.1 sudo: required @@ -15,5 +14,4 @@ before_script: script: - cd $TRAVIS_BUILD_DIR - mix compile - - THRIFT='docker run -v "$PWD:/thrash" -w /thrash thrift:0.9.3 thrift' THRIFT_INPUT_DIR=test/ mix compile.thrift - mix test diff --git a/README.md b/README.md index 4cedbcf..115c758 100644 --- a/README.md +++ b/README.md @@ -128,15 +128,6 @@ usage details. Note, both of these mixins accept a `source` argument to allow you to manually define the source structure in your Thrift IDL. -## Mix.Tasks.Compile.Thrift - -Thrash provides the `compile.thrift` mix task to help with compiling -Elixir projects that use Thrift and Thrash. - -See [lib/mix/tasks/compile/thrift.ex](lib/mix/tasks/compile/thrift.ex) -for detailed documentation of the compile task, including how to -modify your mix.exs file so that this task runs automatically. - ## Data Types Thrift data types are mapped to Elixir as follows. @@ -238,11 +229,7 @@ source from the test thrift file. ``` # fetch deps mix deps.fetch -# make sure the compile.thrift task is available mix compile -# compile thrift -THRIFT_INPUT_DIR=test/ mix compile.thrift -# now tests should work mix test ``` diff --git a/config/config.exs b/config/config.exs index 07140ab..dd6eb19 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,29 +2,11 @@ # and its dependencies with the aid of the Mix.Config module. use Mix.Config -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. +# If this ever gets more complex, switch to separate .exs files +defmodule Config do + def idl_files(:test), do: Path.wildcard("test/thrift/**/*.thrift") + def idl_files(_), do: [] +end -# You can configure for your application as: -# -# config :thrash, key: :value -# -# And access this configuration in your application as: -# -# Application.get_env(:thrash, :key) -# -# Or configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" +config :thrash, + idl_files: Config.idl_files(Mix.env) diff --git a/lib/mix/tasks/compile/thrift.ex b/lib/mix/tasks/compile/thrift.ex deleted file mode 100644 index 9dc223f..0000000 --- a/lib/mix/tasks/compile/thrift.ex +++ /dev/null @@ -1,128 +0,0 @@ -defmodule Mix.Tasks.Compile.Thrift do - @moduledoc """ - Provides a mix task for compiling Thrift IDL files to Erlang. - - Once Thrash is - compiled, you can execute `mix compile.thrift` to generate Erlang code - (a required precursor for Thrash) from your Thrift IDL files (i.e., - `.thrift` files). By default, `mix compile.thrift` assumes that your - IDL files are in the `thrift` directory and that the output should go - in the `src` directory. - - The following environment variables modify the behavior of `mix - compile.thrift`. - - * `THRIFT` - Path to the `thrift` binary (default: `thrift`). - * `THRIFT_INPUT_DIR` - Directory containing your `.thrift` files - (default: `thrift`). - * `THRIFT_OUTPUT_DIR` - Directory in which generated Erlang - source code is placed (default: `src`). - * `FORCE_THRIFT` - Set to any of `["TRUE", "true", "1"]` to force - execution of `thrift`. By default, the task automatically determines - if it is necessary to execute `thrift` based on the mtimes of the - files in the input and output directories. - - Prepend `:thrift` to the list of compilers in your project - and this task will run automatically as needed. - - ``` - defmodule MyProject.Mixfile do - use Mix.Project - - def project do - [app: :my_project, - # usual stuff .. - - # prepend thrift to the usual list of compilers - compilers: [:thrift] ++ Mix.compilers - - # ... - ] - end - end - ``` - - Run `mix deps.compile` first to ensure that the `compile.thrift` task - is available. - """ - - use Mix.Task - - def run(_args) do - options = get_env_options() - - File.mkdir_p!(options[:thrift_output_dir]) - - input_files = thrift_files(options[:thrift_input_dir]) - output_files = generated_files(options[:thrift_output_dir]) - - if require_compile?(options[:force_thrift], input_files, output_files) do - run_thrift(options[:thrift], - options[:thrift_input_dir], - options[:thrift_output_dir]) - end - end - - defp get_env_options() do - %{ - thrift: System.get_env("THRIFT") || "thrift", - thrift_input_dir: System.get_env("THRIFT_INPUT_DIR") || "thrift", - thrift_output_dir: System.get_env("THRIFT_OUTPUT_DIR") || "src", - force_thrift: force_thrift?(System.get_env("FORCE_THRIFT") || false) - } - end - - defp thrift_files(thrift_input_dir) do - Mix.Utils.extract_files([thrift_input_dir], ["thrift"]) - end - - defp run_thrift_on(f, thrift_bin, thrift_output_dir) do - cmd = thrift_bin <> " -o #{thrift_output_dir} --gen erl #{f}" - IO.puts cmd - 0 = Mix.shell.cmd(cmd) - end - - defp run_thrift(thrift_bin, thrift_input_dir, thrift_output_dir) do - thrift_input_dir - |> thrift_files - |> Enum.each(fn(f) -> run_thrift_on(f, thrift_bin, thrift_output_dir) end) - end - - defp generated_files(output_dir) do - Mix.Utils.extract_files([Path.join(output_dir, "gen-erl")], ["hrl", "erl"]) - end - - defp force_thrift?("TRUE"), do: true - defp force_thrift?("true"), do: true - defp force_thrift?("1"), do: true - defp force_thrift?(_), do: false - - defp require_compile?(true, _, _), do: true - defp require_compile?(false, _, []), do: true - defp require_compile?(false, input_files, output_files) do - input_stats = stats_by_mtime(input_files) - output_stats = stats_by_mtime(output_files) - - most_recent(input_stats) > least_recent(output_stats) - end - - defp stats_by_mtime(files) do - Enum.sort_by(file_stats(files), fn(stat) -> stat.mtime end) - end - - defp file_stats(files) do - Enum.map(files, fn(file) -> - File.stat!(file, time: :posix) - end) - end - - defp most_recent([]), do: 0 - defp most_recent([h | _t]), do: h.mtime - - # note x < :infinity is true for any integer - defp least_recent([]), do: :infinity - defp least_recent(list) do - last = List.last(list) - last.mtime - end -end diff --git a/lib/thrash/constants.ex b/lib/thrash/constants.ex index 7b3a563..7636637 100644 --- a/lib/thrash/constants.ex +++ b/lib/thrash/constants.ex @@ -18,22 +18,18 @@ defmodule Thrash.Constants do `MyApp.Constants.magic_number/0`. """ + alias Thrash.IDL alias Thrash.ThriftMeta defmacro __using__(_opts \\ []) do - constants = ThriftMeta.erl_gen_path - |> ThriftMeta.constants_headers - |> Enum.map(fn(header) -> - namespace = ThriftMeta.constants_namespace(header) - - header - |> ThriftMeta.read_constants_exclusive - |> ThriftMeta.thrashify_constants(namespace) - end) - |> List.flatten + caller = __CALLER__.module + caller_namespace = Thrash.MacroHelpers.find_namespace(caller) + + constants = IDL.parse + |> ThriftMeta.read_constants(caller_namespace) Enum.map(constants, fn({k, v}) -> - defconst(k, v) + defconst(k, Macro.escape(v)) end) end diff --git a/lib/thrash/enumerated.ex b/lib/thrash/enumerated.ex index 96930d5..f386aa5 100644 --- a/lib/thrash/enumerated.ex +++ b/lib/thrash/enumerated.ex @@ -46,6 +46,7 @@ defmodule Thrash.Enumerated do """ alias Thrash.ThriftMeta + alias Thrash.IDL alias Thrash.MacroHelpers # there is probably a more idiomatic way to do a lot of this.. @@ -60,7 +61,10 @@ defmodule Thrash.Enumerated do # if you get ":enum_not_found" here, it indicates that the enum # you were looking for does not exist in the thrift-generated # erlang code - quoted_map = module |> find_in_thrift |> ensure_quoted + quoted_map = IDL.parse + |> ThriftMeta.read_enum(module) + |> Macro.escape + map_keys = get_keys(quoted_map) map_values = get_values(quoted_map) reversed = build_reverse(quoted_map) @@ -112,21 +116,10 @@ defmodule Thrash.Enumerated do end end - defp find_in_thrift(modname) do - ThriftMeta.find_in_thrift(fn(h) -> - ThriftMeta.read_enum(h, modname) - end, :enum_not_found) - end - defp reverse_kv(kv) do Enum.map(kv, fn({k, v}) -> {v, k} end) end - defp ensure_quoted(m) when is_map(m) do - {:%{}, [line: __ENV__.line], Enum.into(m, [])} - end - defp ensure_quoted(m), do: m - defp build_reverse({:%{}, line, kv}) do {:%{}, line, reverse_kv(kv)} end diff --git a/lib/thrash/idl.ex b/lib/thrash/idl.ex new file mode 100644 index 0000000..d3c35bc --- /dev/null +++ b/lib/thrash/idl.ex @@ -0,0 +1,57 @@ +defmodule Thrash.IDL do + alias Thrift.Parser + alias Thrift.Parser.Models + + def parse do + parse(Application.get_env(:thrash, :idl_files)) + end + + def parse(junk) when junk == [] or is_nil(junk) do + raise ArgumentError, message: "No IDL files found." + end + def parse(idl_files) do + idl_files + |> Enum.reduce(%Models.Schema{}, fn(path, full_schema) -> + file_idl = Parser.parse(File.read!(path)) + merge(full_schema, file_idl) + end) + end + + def merge( + accum = %Models.Schema{}, + el = %Models.Schema{}) do + %{accum | + constants: merge_maps(accum.constants, el.constants), + enums: merge_maps(accum.enums, el.enums), + exceptions: merge_maps(accum.exceptions, el.exceptions), + includes: merge_includes(accum.includes, el.includes), + namespaces: merge_maps(accum.namespaces, el.namespaces), + services: merge_maps(accum.services, el.services), + structs: merge_maps(accum.structs, el.structs), + thrift_namespace: merge_namespaces( + accum.thrift_namespace, + el.thrift_namespace + ), + typedefs: merge_maps(accum.typedefs, el.typedefs), + unions: merge_maps(accum.unions, el.unions) + } + end + + def constants(idl = %Models.Schema{}) do + Map.get(idl, :constants) + end + + defp merge_maps(m1, m2) do + Map.merge(m1, m2) + end + + defp merge_includes(i1, i2) do + i1 ++ i2 + end + + defp merge_namespaces(nil, nil), do: nil + defp merge_namespaces(s1, s2) do + IO.puts("NAMESPACES: #{inspect s1} #{inspect s2}") + s2 + end +end diff --git a/lib/thrash/macro_helpers.ex b/lib/thrash/macro_helpers.ex index 8b599e6..65d747e 100644 --- a/lib/thrash/macro_helpers.ex +++ b/lib/thrash/macro_helpers.ex @@ -87,6 +87,25 @@ defmodule Thrash.MacroHelpers do quoted_chained_or(rest, {:|, [], [b, a]}) end + @doc """ + Convert an elixir module to an atom (without the Elixir prefix) + """ + @spec unelixir_atom(atom) :: atom + def unelixir_atom(module_name) do + parts = module_name + |> Atom.to_string + |> String.split(".") + + unelixired_parts = case parts do + ["Elixir" | rest] -> rest + other -> other + end + + unelixired_parts + |> Enum.join(".") + |> String.to_atom + end + defp quoted_chained_or([], ast) do ast end diff --git a/lib/thrash/struct_def.ex b/lib/thrash/struct_def.ex index ddbb474..bfc89cd 100644 --- a/lib/thrash/struct_def.ex +++ b/lib/thrash/struct_def.ex @@ -30,21 +30,32 @@ defmodule Thrash.StructDef do name :: atom, default :: term} - alias Thrash.ThriftMeta alias Thrash.MacroHelpers + alias Thrash.IDL @spec find_in_thrift(atom, MacroHelpers.namespace) :: t def find_in_thrift(modulename, namespace) do - ThriftMeta.find_in_thrift(fn(h) -> - ThriftMeta.read_struct(h, modulename, namespace) - end, :struct_not_found) + idl = IDL.parse + modulename = Thrash.ThriftMeta.last_part_of_atom_as_atom(modulename) + {:ok, struct_def} = read(idl, modulename, namespace) + struct_def end - @spec read(atom, atom, MacroHelpers.namespace) :: {:ok, t} | {:error, []} - def read(modulename, struct_name, namespace) do + @spec read(Thrift.Parser.Models.Schema.t, atom, MacroHelpers.namespace) :: {:ok, t} | {:error, []} + def read(idl, struct_name, namespace) do struct_name - |> try_reading_struct_from(modulename) - |> maybe_do(fn(struct_info) -> from_struct_info(namespace, struct_info) end) + |> try_reading_struct_from(idl) + |> maybe_do(fn(struct_info) -> from_thrift_struct(namespace, struct_info) end) + end + + def from_thrift_struct(namespace, %Thrift.Parser.Models.Struct{fields: fields}) do + Enum.map(fields, fn(field) -> + %Field{id: field.id, + required: undefined_to_nil(field.required), + type: translate_type(field.type, namespace), + name: field.name, + default: translate_default(field.type, field.default, namespace)} + end) end @spec from_struct_info(MacroHelpers.namespace, {:struct, [thrift_field]}) :: t @@ -91,10 +102,14 @@ defmodule Thrash.StructDef do end defp maybe_do({:error, x}, _f), do: {:error, x} + defp translate_type(:i8, _namespace), do: :byte defp translate_type({:struct, {_from_mod, struct_module}}, namespace) do {:struct, MacroHelpers.atom_to_elixir_module(struct_module, namespace)} end - defp translate_type({:map, from_type, to_type}, namespace) do + defp translate_type(%Thrift.Parser.Models.StructRef{referenced_type: struct_module}, namespace) do + {:struct, MacroHelpers.atom_to_elixir_module(struct_module, namespace)} + end + defp translate_type({:map, {from_type, to_type}}, namespace) do {:map, {translate_type(from_type, namespace), translate_type(to_type, namespace)}} end @@ -104,26 +119,28 @@ defmodule Thrash.StructDef do defp translate_type({:list, of_type}, namespace) do {:list, translate_type(of_type, namespace)} end - defp translate_type(other_type, _namespace), do: other_type + defp translate_type(other_type, _namespace) do + other_type + end - defp translate_default({:struct, {_thrift_namespace, struct_module}}, + defp translate_default(%Thrift.Parser.Models.StructRef{referenced_type: struct_module}, _, namespace) do struct_module = MacroHelpers.atom_to_elixir_module(struct_module, namespace) {:defer_struct, struct_module} end - defp translate_default(:bool, :undefined, _namespace), do: false - defp translate_default({:map, _, _}, :undefined, _namespace), do: %{} - defp translate_default({:map, _, _}, default_map, _namespace) do + defp translate_default(:bool, nil, _namespace), do: false + defp translate_default({:map, {_, _}}, nil, _namespace), do: %{} + defp translate_default({:map, {_, _}}, default_map, _namespace) do default_map |> :dict.to_list |> Enum.into(%{}) end - defp translate_default({:set, _}, :undefined, _namespace), do: MapSet.new + defp translate_default({:set, _}, nil, _namespace), do: MapSet.new defp translate_default({:set, _}, default_set, _namespace) do :sets.fold(fn(el, acc) -> MapSet.put(acc, el) end, MapSet.new, default_set) end - defp translate_default({:list, _}, :undefined, _namespace), do: [] - defp translate_default(_, :undefined, _namespace), do: nil + defp translate_default({:list, _}, nil, _namespace), do: [] + defp translate_default(_, nil, _namespace), do: nil defp translate_default(_, default, _namespace), do: default defp undefined_to_nil(:undefined), do: nil @@ -148,18 +165,20 @@ defmodule Thrash.StructDef do end defp collapse_deferred_defaults({:defer_struct, struct_module}) do - struct_module.__struct__ + if :erlang.function_exported(struct_module, :__struct__, 0) do + struct_module.__struct__ + else + nil + end end defp collapse_deferred_defaults(default) do default end - defp try_reading_struct_from(struct_name, modulename) do - try do - {:ok, modulename.struct_info_ext(struct_name)} - rescue - _e in FunctionClauseError -> - {:error, []} + defp try_reading_struct_from(struct_name, idl) do + case Map.get(idl.structs, struct_name) do + nil -> {:error, []} + struct_def -> {:ok, struct_def} end end end diff --git a/lib/thrash/thrift_meta.ex b/lib/thrash/thrift_meta.ex index 635cc3c..c2d924f 100644 --- a/lib/thrash/thrift_meta.ex +++ b/lib/thrash/thrift_meta.ex @@ -1,48 +1,13 @@ defmodule Thrash.ThriftMeta do @moduledoc false + alias Thrash.IDL + # Functions to access metadata from the Thrift-generated Erlang code # Thrash internal use only - alias Thrash.StructDef - @type finder :: ((String.t) -> {:ok, term} | {:error, term}) - @doc """ - Returns the path of the thrift-generated erlang files. - - By default this is 'src/erl-gen'. You can override it with the - Application env path `:thrash`/`:erl_gen_path` - """ - @spec erl_gen_path() :: String.t - def erl_gen_path() do - :thrash - |> Application.get_env(:erl_gen_path, "src/gen-erl") - |> Path.expand - end - - @doc """ - Returns a list of erlang header files matching *_types.hrl in any - subdirectory of root_path (usually erl_gen_path/0). - """ - @spec types_headers(String.t) :: [String.t] - def types_headers(root_path) do - root_path - |> Path.join("**/*_types.hrl") - |> Path.wildcard - end - - @doc """ - Returns a list of erlang header files matching *_types.hrl in any - subdirectory of root_path (usually erl_gen_path/0). - """ - @spec constants_headers(String.t) :: [String.t] - def constants_headers(root_path) do - root_path - |> Path.join("**/*_constants.hrl") - |> Path.wildcard - end - @doc """ Determine namespace from constants header file name @@ -57,22 +22,14 @@ defmodule Thrash.ThriftMeta do end @doc """ - Read the `-define`d constants from a thrift-generated header file. - - Mostly a passthrough to Quaff.Constants.get_constants, but strips - out some of the metadata that it returns. + Read constants from a thrift IDL file """ - @spec read_constants(String.t) :: Keyword.t - def read_constants(header_file) do - basename = Path.basename(header_file, ".hrl") - included_tag = String.to_atom("_" <> basename <> "_included") - meta_constants = [:MODULE_STRING, :FILE, :MODULE, included_tag] - - header_file - |> Quaff.Constants.get_constants([]) - |> Enum.filter(fn({k, _v}) -> - !Enum.member?(meta_constants, k) - end) + @spec read_constants(Thrift.Parser.Models.Schema.t, Module.t | nil) :: Keyword.t + def read_constants(idl, namespace) do + idl + |> IDL.constants + |> thrashify_constants(idl, namespace) + |> Enum.into(%{}) end @doc """ @@ -80,112 +37,112 @@ defmodule Thrash.ThriftMeta do e.g., `[FOO_THING: 42], "FOO_"` -> `[thing: 42]` """ - @spec thrashify_constants(Keyword.t, String.t) :: Keyword.t - def thrashify_constants(constants, namespace) do + @spec thrashify_constants(Keyword.t, Thrift.Parser.Models.Schema.t, Module.t | nil) :: Keyword.t + def thrashify_constants(constants, idl, namespace) do Enum.map(constants, - fn({k, v}) -> {thrift_to_thrash_const(k, namespace), v} end) + fn({k, v}) -> + {thrift_to_thrash_const(k), translate_constant(v, idl, namespace)} + end) + end + + defp translate_constant( + %Thrift.Parser.Models.Constant{ + type: %Thrift.Parser.Models.StructRef{referenced_type: referenced_type}, + value: value + }, + idl, + namespace + ) do + struct_module = Thrash.MacroHelpers.atom_to_elixir_module(referenced_type, namespace) + struct = struct_module.__struct__ + struct_def = find_struct(referenced_type, idl) + Enum.reduce(value, struct, fn({k, v}, acc) -> + k = List.to_atom(k) + Map.update!(acc, k, fn(_) -> + translate_value(v, k, struct_def) + end) + end) end - - @doc """ - Read constants from a header file, exclude constants from included headers - """ - @spec read_constants_exclusive(String.t) :: Keyword.t - def read_constants_exclusive(header_file) do - constants = header_file - |> read_constants - |> Enum.into(MapSet.new) - - included = determine_included_libs(constants, header_file) - - included_constants = included - |> Enum.map(&read_constants/1) - |> List.flatten - |> Enum.uniq - |> Enum.into(MapSet.new) - - constants - |> MapSet.difference(included_constants) - |> Enum.into([]) - |> Enum.filter(&is_not_included_lib?/1) + defp translate_constant( + %Thrift.Parser.Models.Constant{ + type: {:list, ref = %Thrift.Parser.Models.StructRef{}}, + value: value + }, + idl, + namespace + ) do + Enum.map(value, fn(v) -> + translate_constant(%Thrift.Parser.Models.Constant{type: ref, value: v}, idl, namespace) + end) + end + defp translate_constant( + %Thrift.Parser.Models.Constant{ + type: {:map, {_from_type, ref = %Thrift.Parser.Models.StructRef{}}}, + value: value + }, + idl, + namespace + ) do + Enum.map(value, fn({k, v}) -> + {k, translate_constant(%Thrift.Parser.Models.Constant{type: ref, value: v}, idl, namespace)} + end) + |> Enum.into(%{}) + end + defp translate_constant(%Thrift.Parser.Models.Constant{value: value}, _, _) do + value end - def determine_included_libs(constants, header_file) do - dir = Path.dirname(header_file) + defp find_struct(name, idl) do + Map.get(idl, :structs) |> Map.get(name) + end - constants - |> Enum.filter(fn(constant_def) -> is_included_lib?(constant_def) end) - |> Enum.map(fn({k, _v}) -> included_tag_to_header(k, dir) end) + defp translate_value(v, k, %Thrift.Parser.Models.Struct{fields: fields}) do + field_def = find_field(fields, k) + translate_value(v, field_def.type) end - @doc """ - Read a struct definition from a header file. + defp find_field(fields, k) do + Enum.find(fields, fn(f) -> f.name == k end) + end - Uses the header name to determine the underlying module name (e.g., - 'foo.hrl' -> ':foo') and calls struct_info. Any namespace module is - removed from the struct_name before calling struct_info (e.g., - 'Foo.Bar' -> 'Bar'). - """ - @spec read_struct(String.t, atom, atom) :: {:ok, StructDef.t} | {:error, []} - def read_struct(header_file, struct_name, namespace) do - basename = Path.basename(header_file, ".hrl") - modulename = String.to_atom(basename) - struct_name = last_part_of_atom_as_atom(struct_name) - StructDef.read(modulename, struct_name, namespace) + defp translate_value(list_string, :string) do + List.to_string(list_string) end + defp translate_value(v, _), do: v @doc """ - Read an enum definition from a header file. + Read an enum definition from thrift IDL file Strips the namespace and enum name and downcases the key names. The enum name is upcased before search, and only the last part of the atom is used (e.g., `MyApp.Things` becomes `THINGS`) """ - @spec read_enum(String.t, atom) :: {:ok, map} | {:error, map} - def read_enum(header_file, enum_name) do - basename = Path.basename(header_file, ".hrl") - namespace_string = String.replace(basename, ~r/_types$/, "") - enum_name_string = last_part_of_atom_as_string(enum_name) - full_namespace = String.upcase(namespace_string <> "_" <> - enum_name_string <> "_") - - header_file - |> read_constants - |> Enum.filter(fn({k, _v}) -> has_namespace?(k, full_namespace) end) - |> Enum.map(fn({k, v}) -> - {thrift_to_thrash_const(k, full_namespace), v} - end) - |> Enum.into(%{}) - |> ok_if_not_empty - end - - @doc """ - Finds a value in thrift-generated Erlang code. - - Iterates over the thrift headers, executes finder, returns the first - value for which finder returns `{:ok, value}`. If no result is - found, error_value is returned. - """ - @spec find_in_thrift(finder, term) :: term - def find_in_thrift(finder, error_value \\ nil) do - headers = types_headers(erl_gen_path()) - Enum.find_value(headers, error_value, fn(h) -> - case finder.(h) do - {:ok, val} -> val - {:error, _} -> nil - end - end) - end - - defp has_namespace?(atom, namespace) do - atom - |> Atom.to_string - |> String.starts_with?(namespace) - end - - defp thrift_to_thrash_const(k, namespace) do + @spec read_enum(Thrift.Parser.Models.Schema.t, atom) :: map + def read_enum(idl, enum_name) do + enum = idl.enums + |> Enum.find(fn({_, enum}) -> name_match?(enum.name, enum_name) end) + + if enum == nil do + raise ArgumentError, message: "Could not find enum #{inspect enum_name}" + else + {_, enum} = enum + enum.values + |> Enum.map(fn({k, v}) -> + {thrift_to_thrash_const(k), v} + end) + |> Enum.into(%{}) + end + end + + defp name_match?(n1, n1), do: true + defp name_match?(n1, n2) do + String.downcase(last_part_of_atom_as_string(n1)) == + String.downcase(last_part_of_atom_as_string(n2)) + end + + defp thrift_to_thrash_const(k) do k |> Atom.to_string - |> String.replace(~r/^#{namespace}/, "") |> String.downcase |> String.to_atom end @@ -197,30 +154,9 @@ defmodule Thrash.ThriftMeta do |> List.last end - defp last_part_of_atom_as_atom(x) do + def last_part_of_atom_as_atom(x) do x |> last_part_of_atom_as_string |> String.to_atom end - - defp ok_if_not_empty(m) when m == %{}, do: {:error, %{}} - defp ok_if_not_empty(m) when is_map(m), do: {:ok, m} - - defp is_included_lib?({k, :yeah}) do - # with a value of :yeah, it's probably an included tag - # but we should be careful - String.match?(Atom.to_string(k), ~r/^_.*_included$/) - end - defp is_included_lib?({_k, _v}), do: false - - defp is_not_included_lib?(x) do - !is_included_lib?(x) - end - - defp included_tag_to_header(tag, dir) do - tag - |> Atom.to_string - |> String.replace(~r/^_(.*)_included$/, "\\1") - |> (&(Path.join(dir, &1 <> ".hrl"))).() - end end diff --git a/mix.exs b/mix.exs index 573747a..e681501 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Thrash.Mixfile do version: "0.1.0", description: description, package: package, - elixir: "~> 1.1", + elixir: "~> 1.2", source_url: "https://github.com/dantswain/thrash", docs: [main: "Thrash"], build_embedded: Mix.env == :prod, @@ -16,7 +16,7 @@ defmodule Thrash.Mixfile do end def application do - [applications: [:logger, :quaff]] + [applications: [:logger, :thrift]] end def elixirc_paths(:bench), do: ["lib", "bench"] @@ -24,18 +24,11 @@ defmodule Thrash.Mixfile do def elixirc_paths(_), do: ["lib"] defp deps do - [{:quaff, - github: "qhool/quaff", - tag: "9a4ba378d470beac708e366dc9bacd5a9ef6f016", - override: true}, - {:earmark, "~> 0.1", only: :dev}, + [{:earmark, "~> 0.1", only: :dev}, + {:thrift, "~> 1.3"}, {:ex_doc, "~> 0.11", only: :dev}, {:dialyze, "~> 0.2", only: :dev}, {:credo, "~> 0.3", only: :dev}, - {:thrift_ex, - github: "dantswain/thrift_ex", - only: :bench, - tag: "f6394871e5685aa1c7e125f198dead0c8a15e992"}, {:exprof, "~>0.2", only: :bench}, {:benchwarmer, "~>0.0.2", only: :bench}] end diff --git a/mix.lock b/mix.lock index 01a4251..beff160 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,9 @@ -%{"aleppo": {:git, "https://github.com/ChicagoBoss/aleppo.git", "e5af421b8c75d86dd88aefae91402478250bb82c", [tag: "v0.9"]}, - "benchwarmer": {:hex, :benchwarmer, "0.0.2"}, - "bunt": {:hex, :bunt, "0.1.5"}, - "credo": {:hex, :credo, "0.3.12"}, - "dialyze": {:hex, :dialyze, "0.2.1"}, - "earmark": {:hex, :earmark, "0.2.1"}, - "ex_doc": {:hex, :ex_doc, "0.11.4"}, - "exprintf": {:hex, :exprintf, "0.1.6"}, - "exprof": {:hex, :exprof, "0.2.0"}, - "quaff": {:git, "https://github.com/qhool/quaff.git", "9a4ba378d470beac708e366dc9bacd5a9ef6f016", [tag: "9a4ba378d470beac708e366dc9bacd5a9ef6f016"]}, - "thrift": {:git, "https://github.com/apache/thrift", "591e20f9636c37527a70dc03598218c3468a0eff", [tag: "0.9.2"]}, - "thrift_ex": {:git, "https://github.com/dantswain/thrift_ex.git", "3fe3341f9dc34219ceb04ae2d3751b9fb5eac37a", [tag: "f6394871e5685aa1c7e125f198dead0c8a15e992"]}} +%{"benchwarmer": {:hex, :benchwarmer, "0.0.2", "902e5c020608647b07c38b82103e4af6d2667dfd5d5d13c67382238de6943136", [:mix], []}, + "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, + "credo": {:hex, :credo, "0.4.12", "f5e1973405ea25c6e64959fb0b6bf92950147a0278cc2a002a491b45f78f7b87", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, + "dialyze": {:hex, :dialyze, "0.2.1", "9fb71767f96649020d769db7cbd7290059daff23707d6e851e206b1fdfa92f9d", [:mix], []}, + "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, + "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}, + "exprintf": {:hex, :exprintf, "0.1.6", "b5b0a38bf78a357dbc61cdf7364fea004af6fdf5214be44021eb2ea7edf61f6e", [:mix], []}, + "exprof": {:hex, :exprof, "0.2.0", "b03f50d0d33e2f18c8e047d9188ba765dc32daba0b553ed717a98a78561d5eaf", [:mix], [{:exprintf, "~> 0.1", [hex: :exprintf, optional: false]}]}, + "thrift": {:hex, :thrift, "1.3.0", "d12c2689ba4dcfa33ecd2c99d783cc205fa2a47176c8c972a7e0e5b137023ffd", [:mix], []}} diff --git a/test/thrash/constants_test.exs b/test/thrash/constants_test.exs index 4470fcd..8908906 100644 --- a/test/thrash/constants_test.exs +++ b/test/thrash/constants_test.exs @@ -3,5 +3,7 @@ defmodule Thrash.ConstantsTest do test "generating constants" do assert 42 == Constants.max_things + + assert %SubStruct{sub_id: 9, sub_name: "number 9"} == Constants.const_substruct end end diff --git a/test/thrash/idl_test.exs b/test/thrash/idl_test.exs new file mode 100644 index 0000000..a38c00c --- /dev/null +++ b/test/thrash/idl_test.exs @@ -0,0 +1,39 @@ +defmodule Thrash.IDLTest do + use ExUnit.Case + + @idl_pattern Path.expand("../thrift", __DIR__) <> "**/*.thrift" + @idl_files Path.wildcard(@idl_pattern) + + alias Thrash.IDL + + setup do + existing_idl = Application.get_env(:thrash, :idl_files) + + on_exit fn -> + Application.put_env(:thrash, :idl_files, existing_idl) + end + end + + test "reading multiplie thrift IDL files" do + thrift_idl = IDL.parse(@idl_files) + assert %Thrift.Parser.Models.Schema{} = thrift_idl + assert [:TacoType] == Map.keys(thrift_idl.enums) + assert [:SimpleStruct, :SubStruct] == Map.keys(thrift_idl.structs) + + # use app env by default + Application.put_env(:thrash, :idl_files, @idl_files) + assert thrift_idl == IDL.parse + end + + test "reading idl fails if there are no idl files" do + Application.put_env(:thrash, :idl_files, nil) + assert_raise ArgumentError, fn -> + _ = IDL.parse + end + + Application.put_env(:thrash, :idl_files, []) + assert_raise ArgumentError, fn -> + _ = IDL.parse + end + end +end diff --git a/test/thrash/struct_def_test.exs b/test/thrash/struct_def_test.exs index ee5a51c..2f56bb5 100644 --- a/test/thrash/struct_def_test.exs +++ b/test/thrash/struct_def_test.exs @@ -1,6 +1,40 @@ defmodule Thrash.StructDefTest do use ExUnit.Case + test "reading a struct def from the idl" do + struct_def = Thrash.StructDef.find_in_thrift(:SubStruct, nil) + assert 2 == length(struct_def) + [f1, _] = struct_def + assert 1 == f1.id + assert nil == f1.default + assert :sub_id == f1.name + assert :i32 == f1.type + end + + test "reading a struct def from the idl with struct fields" do + struct_def = Thrash.StructDef.find_in_thrift(:SimpleStruct, nil) + assert 14 == length(struct_def) + substruct = Enum.at(struct_def, 4) + assert :sub_struct == substruct.name + assert {:struct, SubStruct} == substruct.type + + map = Enum.at(struct_def, 12) + assert :map_string_to_struct == map.name + assert {:map, {:string, {:struct, SubStruct}}} == map.type + end + + test "expanding a struct def" do + struct_def = SimpleStruct + |> Thrash.StructDef.find_in_thrift(nil) + |> Thrash.StructDef.override_types([]) + |> Thrash.StructDef.override_defaults([]) + |> Thrash.StructDef.to_defstruct + + assert [] == Keyword.get(struct_def, :list_of_ints) + assert MapSet.new == Keyword.get(struct_def, :set_of_strings) + assert %{} == Keyword.get(struct_def, :map_string_to_struct) + end + test "building a struct with substructs that are namespaced" do struct = %Namespaced.SimpleStruct{} sub_struct = %Namespaced.SubStruct{} diff --git a/test/thrash/thrift_meta_test.exs b/test/thrash/thrift_meta_test.exs index a221cd5..3de6631 100644 --- a/test/thrash/thrift_meta_test.exs +++ b/test/thrash/thrift_meta_test.exs @@ -1,43 +1,27 @@ defmodule Thrash.ThriftMetaTest do use ExUnit.Case - @gen_erl Path.expand("../../src/gen-erl", __DIR__) + alias Thrash.IDL - test "erl_gen_path works" do - assert @gen_erl == Thrash.ThriftMeta.erl_gen_path() - end - - test "types_headers works" do - assert [Path.join(@gen_erl, "thrash_test_types.hrl")] == - Thrash.ThriftMeta.types_headers(Thrash.ThriftMeta.erl_gen_path()) - end - - test "constants_headers works" do - assert [Path.join(@gen_erl, "thrash_test_constants.hrl")] == - Thrash.ThriftMeta.constants_headers(Thrash.ThriftMeta.erl_gen_path()) - end - - test "reading exclusive constants" do - path = Path.join(@gen_erl, "thrash_test_constants.hrl") - assert [THRASH_TEST_MAX_THINGS: 42] == - Thrash.ThriftMeta.read_constants_exclusive(path) - end + @idl_pattern Path.expand("../thrift", __DIR__) <> "**/*.thrift" + @idl_files Path.wildcard(@idl_pattern) test "read_constants works" do - [header] = Thrash.ThriftMeta.types_headers(Thrash.ThriftMeta.erl_gen_path()) + idl = IDL.parse(@idl_files) - expected = [THRASH_TEST_TACOTYPE_BARBACOA: 123, - THRASH_TEST_TACOTYPE_CARNITAS: 124, - THRASH_TEST_TACOTYPE_STEAK: 125, - THRASH_TEST_TACOTYPE_CHICKEN: 126, - THRASH_TEST_TACOTYPE_PASTOR: 127] |> Enum.into(HashSet.new) - got = Thrash.ThriftMeta.read_constants(header) |> Enum.into(HashSet.new) + expected = %{ + max_things: 42, + const_substruct: %SubStruct{sub_id: 9, sub_name: "number 9"}, + const_substruct_list: [%SubStruct{sub_id: 10, sub_name: "number 10"}], + const_substruct_map: %{1 => %SubStruct{sub_id: 1}} + } + got = Thrash.ThriftMeta.read_constants(idl, nil) - assert Set.equal?(expected, got), "#{inspect expected} was not eq #{inspect got}" + assert expected == got end test "read_enum works" do - [header] = Thrash.ThriftMeta.types_headers(Thrash.ThriftMeta.erl_gen_path()) + idl = IDL.parse(@idl_files) expected = %{barbacoa: 123, carnitas: 124, @@ -45,20 +29,21 @@ defmodule Thrash.ThriftMetaTest do chicken: 126, pastor: 127} - got = Thrash.ThriftMeta.read_enum(header, TacoType) - assert {:ok, expected} == got + got = Thrash.ThriftMeta.read_enum(idl, TacoType) + assert expected == got - got = Thrash.ThriftMeta.read_enum(header, TACOTYPE) - assert {:ok, expected} == got + got = Thrash.ThriftMeta.read_enum(idl, TACOTYPE) + assert expected == got - got = Thrash.ThriftMeta.read_enum(header, ThrashFoo.TacoType) - assert {:ok, expected} == got + got = Thrash.ThriftMeta.read_enum(idl, ThrashFoo.TacoType) + assert expected == got end - test "read_enum returns {:error, %{} if the enum is not found" do - [header] = Thrash.ThriftMeta.types_headers(Thrash.ThriftMeta.erl_gen_path()) + test "read_enum raises an error if the enum is not found" do + idl = IDL.parse(@idl_files) - assert {:error, %{}} == - Thrash.ThriftMeta.read_enum(header, BurritoType) + assert_raise ArgumentError, fn -> + Thrash.ThriftMeta.read_enum(idl, BurritoType) + end end end diff --git a/test/thrift/constants.thrift b/test/thrift/constants.thrift new file mode 100644 index 0000000..8c24310 --- /dev/null +++ b/test/thrift/constants.thrift @@ -0,0 +1,7 @@ +include "simple_struct.thrift" + +const i32 MAX_THINGS = 42 + +const SubStruct const_substruct = {"sub_id": 9, "sub_name": "number 9"} +const list const_substruct_list = [{"sub_id": 10, "sub_name": "number 10"}] +const map const_substruct_map = {1: {"sub_id": 1}} diff --git a/test/thrash_test.thrift b/test/thrift/simple_struct.thrift similarity index 64% rename from test/thrash_test.thrift rename to test/thrift/simple_struct.thrift index 2940d0d..9a86b1e 100644 --- a/test/thrash_test.thrift +++ b/test/thrift/simple_struct.thrift @@ -1,19 +1,5 @@ -namespace erl thrash - -const i32 MAX_THINGS = 42 - -enum TacoType { - BARBACOA = 123, - CARNITAS = 124, - STEAK = 125, - CHICKEN = 126, - PASTOR = 127 -} - -struct SubStruct { - 1: i32 sub_id - 2: string sub_name -} +include "taco_type.thrift" +include "sub_struct.thrift" struct SimpleStruct { 1: i32 id diff --git a/test/thrift/sub_struct.thrift b/test/thrift/sub_struct.thrift new file mode 100644 index 0000000..93ac0a9 --- /dev/null +++ b/test/thrift/sub_struct.thrift @@ -0,0 +1,4 @@ +struct SubStruct { + 1: i32 sub_id + 2: string sub_name +} diff --git a/test/thrift/taco_type.thrift b/test/thrift/taco_type.thrift new file mode 100644 index 0000000..1a472cc --- /dev/null +++ b/test/thrift/taco_type.thrift @@ -0,0 +1,7 @@ +enum TacoType { + BARBACOA = 123, + CARNITAS = 124, + STEAK = 125, + CHICKEN = 126, + PASTOR = 127 +}