├── .gitignore ├── README.md ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib ├── mix │ └── tasks │ │ └── compile.protocol_ex.ex └── protocol_ex.ex ├── mix.exs ├── mix.lock ├── test ├── bench │ └── numbers_test.exs ├── protocol_ex_test.exs └── test_helper.exs └── tests_extra ├── consolidate_test ├── .gitignore ├── README.md ├── config │ └── config.exs ├── lib │ ├── blah.ex │ ├── blah_impls.ex │ └── consolidate_test.ex ├── mix.exs ├── mix.lock └── test │ ├── consolidate_test_test.exs │ └── test_helper.exs └── umbrella_test ├── .formatter.exs ├── .gitignore ├── README.md ├── apps ├── app1 │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── app1.ex │ │ └── blah_impls.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── app1_test.exs │ │ └── test_helper.exs ├── lib1 │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── blah.ex │ │ └── lib1.ex │ ├── mix.exs │ └── test │ │ ├── lib1_test.exs │ │ └── test_helper.exs ├── lib2 │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── lib2.ex │ ├── mix.exs │ └── test │ │ ├── lib2_test.exs │ │ └── test_helper.exs └── lib3 │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── lib3.ex │ ├── mix.exs │ └── test │ ├── lib3_test.exs │ └── test_helper.exs ├── config └── config.exs └── mix.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temp files 23 | .#* 24 | *.swp 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProtocolEx 2 | 3 | Extended Protocol library. 4 | 5 | Performs matching for protocol implementations instead of being limited to certain base types as in standard Elixir Protocols. 6 | 7 | ## Installation 8 | 9 | [Available in Hex](https://hex.pm/packages/protocol_ex) with [Documentation](https://hexdocs.pm/protocol_ex), the package can be installed 10 | by adding `:protocol_ex` to your list of dependencies in `mix.exs`: 11 | 12 | ```elixir 13 | {:protocol_ex, "~> 0.3.0"}, 14 | ``` 15 | 16 | ## Usage 17 | 18 | For auto-consolidation add the compiler to your `mix.exs` definition like (make certain it comes after the built-in elixir compiler): 19 | 20 | ```elixir 21 | def project do 22 | [ 23 | # ... 24 | compilers: Mix.compilers ++ [:protocol_ex], 25 | # ... 26 | ] 27 | end 28 | ``` 29 | 30 | ### Setup 31 | 32 | The below assumes: 33 | 34 | ```elixir 35 | import ProtocolEx 36 | ``` 37 | 38 | ### `defprotocol_ex/2` 39 | 40 | `defprotocol_ex/2` is used like `defmodule` in that it takes a module name to become and the body. The body can contain plain function heads like: 41 | 42 | ```elixir 43 | def something(a) 44 | def blah(a, b) 45 | ``` 46 | 47 | Or it can contain full bodies: 48 | 49 | ```elixir 50 | def bloop(a) do 51 | to_string(a) 52 | end 53 | ``` 54 | 55 | Plain heads **must** be implemented in an implementation, not doing so will raise an error. 56 | 57 | Full body functions supply the fallback, if an implementation does not supply an implementation of it then it will fall back to the fallback implementation. 58 | 59 | Inside a `defprotocol_ex/2` you are able to use `deftest` to run some tests at compile time to make certain that the implementations follow necessary rules. 60 | 61 | #### Example 62 | 63 | ```elixir 64 | defprotocol_ex Blah do 65 | def empty() # Transformed to 1-arg that matches on based on the implementation, but ignored otherwise 66 | def succ(a) 67 | def add(a, b) 68 | def map(a, f) when is_function(f, 1) 69 | 70 | def a_fallback(a), do: inspect(a) 71 | end 72 | ``` 73 | 74 | ##### `deftest` example 75 | 76 | In this example each implementation must also define a `prop_generator` that returns a StreamData generator to generate the types of that implementation, such as for lists: `def prop_generator(), do: StreamData.list_of(StreamData.integer())` 77 | 78 | ```elixir 79 | defprotocol_ex Functor do 80 | def map(v, f) 81 | 82 | deftest identity do 83 | StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v -> 84 | if v === map(v, &(&1)) do 85 | {:ok, v} 86 | else 87 | {:error, v} 88 | end 89 | end) 90 | end 91 | 92 | deftest composition do 93 | f = fn x -> x end 94 | g = fn x -> x end 95 | StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v -> 96 | if map(v, fn x -> f.(g.(x)) end) === map(map(v, g), f) do 97 | {:ok, v} 98 | else 99 | {:error, v} 100 | end 101 | end) 102 | end 103 | end 104 | ``` 105 | 106 | You can cancel running tests on compile by passing `--no-protocol-tests` to the `mix compile` command. 107 | 108 | ##### Named position example 109 | 110 | You can also specify a name to the matcher so you can use the same name in a specific position in a `def`, like in: 111 | 112 | ```elixir 113 | defprotocol_ex Monad, as: monad do 114 | def wrap(value, monad) 115 | def flat_map(monad, fun) 116 | end 117 | ``` 118 | 119 | In this example `wrap/2` uses the monad matcher in its last position, where `flat_map/2` uses it in the first. 120 | 121 | ### `defimpl_ex/4` 122 | 123 | `defimpl_ex/4` takes a unique name for this implementation for the given protocol first, then a normal elixir match expression second, then `[for: ProtocolName]` for a given protocol, and lastly the body. 124 | 125 | #### Example 126 | 127 | ```elixir 128 | defimpl_ex Integer, i when is_integer(i), for: Blah do 129 | def empty(), do: 0 130 | defmacro succ(i), do: quote(do: unquote(i)+1) # Macro's get inlined into the protocol itself 131 | def add(i, b), do: i+b 132 | def map(i, f), do: f.(i) 133 | 134 | def a_fallback(i), do: "Integer: #{i}" 135 | end 136 | 137 | defimpl_ex TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do 138 | def empty(), do: {Vwoop, 0} 139 | def succ({Vwoop, i}), do: {Vwoop, i+1} 140 | def add({Vwoop, i}, b), do: {Vwoop, i+b} 141 | def map({Vwoop, i}, f), do: {Vwoop, f.(i)} 142 | end 143 | 144 | defmodule MyStruct do 145 | defstruct a: 42 146 | end 147 | 148 | defimpl_ex MineOlStruct, %MyStruct{}, for: Blah do 149 | def empty(), do: %MyStruct{a: 0} 150 | def succ(s), do: %{s | a: s.a+1} 151 | def add(s, b), do: %{s | a: s.a+b} 152 | def map(s, f), do: %{s | a: f.(s.a)} 153 | end 154 | ``` 155 | 156 | ### `resolve_protocol_ex/2` 157 | 158 | `resolve_protocol_ex/2` allows to dynamic consolidation (or if you do not wish to use the compiler). It takes the protocol module name first, then a list of the unique names to consolidate. If there is more than one implementation that can match a given value then they are used in the order of definition here. 159 | 160 | #### Example 161 | 162 | ```elixir 163 | ProtocolEx.resolve_protocol_ex(Blah, [ 164 | Integer, 165 | TaggedTuple.Vwoop, 166 | MineOlStruct, 167 | ]) 168 | ``` 169 | 170 | This can be called *again* at runtime if so wished, it allows you rebuild the protocol consolidation module to remove or add implementations such as for dynamic plugins. 171 | 172 | ### Protocol Usage 173 | 174 | To use your protocol you just call the specific functions on the module, for the above examples then all of these will work: 175 | 176 | ```elixir 177 | 0 = Blah.empty(42) 178 | {Vwoop, 0} = Blah.empty({Vwoop, 42}) 179 | %MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42}) 180 | 181 | 43 = Blah.succ(42) 182 | {Vwoop, 43} = Blah.succ({Vwoop, 42}) 183 | %MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42}) 184 | 185 | 47 = Blah.add(42, 5) 186 | {Vwoop, 47} = Blah.add({Vwoop, 42}, 5) 187 | %MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5) 188 | 189 | "Integer: 42" = Blah.a_fallback(42) 190 | "{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42}) 191 | "%MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42}) 192 | 193 | 43 = Blah.map(42, &(&1+1)) 194 | {Vwoop, 43} = Blah.map({Vwoop, 42}, &(&1+1)) 195 | %MyStruct{a: 43} = Blah.map(%MyStruct{a: 42}, &(&1+1)) 196 | ``` 197 | 198 | It can of course be useful to call an implementation directly as well: 199 | 200 | ```elixir 201 | 0 = Blah.Integer.empty() 202 | {Vwoop, 0} = Blah.TaggedTuple.Vwoop.empty() 203 | %MyStruct{a: 0} = Blah.MineOlStruct.empty() 204 | ``` 205 | 206 | ## Debugging 207 | 208 | As with the standard Elixir Protocols, running `mix compile` with the `--verbose` flag like `mix compile --verbose` will state what protocol\_ex's it found and with what implementations it found to combine it with. 209 | 210 | You can also use the `--print-protocol-ex` flag to print out the resultant compiled protocol source itself. Do note that this source file itself may not be compileable and/or is very unlikely to work as Elixirs AST is not homioiconic and thus it loses specific contextual information. 211 | 212 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | 4 | config :stream_data, initial_size: 1 5 | config :stream_data, max_runs: 100 6 | config :stream_data, max_shrinking_steps: 100 7 | 8 | 9 | import_config "#{Mix.env}.exs" 10 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :stream_data, initial_size: 1 4 | config :stream_data, max_runs: 1000 5 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.protocol_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.ProtocolEx do 2 | use Mix.Task 3 | 4 | @spec run(OptionParser.argv()) :: :ok 5 | def run(args) do 6 | # config = Mix.Project.config 7 | # Mix.Task.run "compile", args 8 | {opts, _, _} = 9 | OptionParser.parse(args, switches: [ 10 | verbose: :boolean, 11 | print_protocol_ex: :boolean, 12 | no_protocol_tests: :boolean, 13 | ]) 14 | 15 | verbose = opts[:verbose] 16 | 17 | opts = 18 | if opts[:no_protocol_tests] do 19 | opts 20 | else 21 | Keyword.put_new(opts, :protocol_tests, []) 22 | end 23 | 24 | if(verbose, do: IO.puts("Consolidating ProtocolEx's project-wide...")) 25 | ProtocolEx.consolidate_all([output_beam: true] ++ opts) 26 | if(verbose, do: IO.puts("Consolidating ProtocolEx's project-wide complete.")) 27 | :ok 28 | end 29 | 30 | @doc """ 31 | Cleans up consolidated protocols. 32 | """ 33 | def clean do 34 | config = Mix.Project.config() 35 | File.rm_rf(Mix.Project.consolidation_path(config)) 36 | :ok 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/protocol_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule ProtocolEx do 2 | @moduledoc """ 3 | Matcher protocol control module 4 | """ 5 | 6 | @no_match :__@@NO_MATCH@@__ 7 | 8 | defmodule InvalidProtocolSpecification do 9 | @moduledoc """ 10 | This is raised when a protocol definition is invalid. 11 | 12 | If a new feature is wanted in the protocol definition, please raise an issue or submit a PR. 13 | """ 14 | defexception [ast: nil] 15 | def message(exc), do: "Unhandled specification node: #{inspect exc.ast}" 16 | end 17 | 18 | defmodule MissingAsInArgs do 19 | @moduledoc """ 20 | This is raised when an `as` name is specified but is missing from a function argument list. 21 | """ 22 | defexception [as: nil, ast: nil] 23 | def message(exc), do: "Missing required name #{exc.as} in: #{inspect exc.ast}" 24 | end 25 | 26 | defmodule DuplicateSpecification do 27 | @moduledoc """ 28 | Only one implementation for a given callback per implementaiton is allowed at this time. 29 | """ 30 | defexception [name: nil, arity: 0] 31 | def message(exc) do 32 | if exc.arity === -1 do 33 | "Cannot specify both a 0-arity and 1-arity version of the same function: #{inspect exc.name}" 34 | else 35 | "Duplicate specification node: #{inspect exc.name}/#{inspect exc.arity}" 36 | end 37 | end 38 | end 39 | 40 | defmodule UnimplementedProtocolEx do 41 | @moduledoc """ 42 | Somehow a given implementation was consolidated without actually having a required callback specified. 43 | """ 44 | defexception [proto: nil, name: nil, arity: 0, value: nil] 45 | def message(exc), do: "Unimplemented Protocol of `#{exc.proto}` at #{inspect exc.name}/#{inspect exc.arity} with arguments: #{inspect exc.value}" 46 | end 47 | 48 | defmodule MissingRequiredProtocolDefinition do 49 | @moduledoc """ 50 | The given implementation is missing a required callback from the protocol. 51 | """ 52 | defexception [proto: nil, impl: nil, name: nil, arity: -1] 53 | def message(exc) do 54 | impl = String.replace_prefix(to_string(exc.impl), to_string(exc.proto)<>".", "") 55 | "On Protocol `#{exc.proto}` missing a required protocol callback on `#{impl}` of: #{exc.name}/#{exc.arity}" 56 | end 57 | end 58 | 59 | defmodule ProtocolExTestFailure do 60 | @moduledoc """ 61 | A failed test and information about the error condition 62 | """ 63 | defexception [proto: nil, type: nil, name: nil, meta: nil, value: nil] 64 | def message(exc) do 65 | location = inspect(exc.meta) 66 | "\nOn Protocol `#{exc.proto}`\n\twith type of `#{exc.type}`,\n\tfailed test `#{exc.name}`/#{location}\n\twith error value of: #{inspect exc.value}\n\n" 67 | end 68 | end 69 | 70 | 71 | defmodule Spec do 72 | @moduledoc false 73 | defstruct [ 74 | callbacks: [], 75 | as: nil, 76 | location: [], 77 | docs: %{}, 78 | head_asts: [], 79 | cache: %{}, # Used only at compile-time, cleared before saving 80 | ] 81 | end 82 | 83 | def clean_spec(%Spec{} = spec) do 84 | %{spec| 85 | head_asts: [], 86 | cache: %{}, 87 | } 88 | end 89 | 90 | 91 | @desc_name :"$ProtocolEx_description$" 92 | @desc_attr :protocol_ex_desc 93 | 94 | 95 | @doc """ 96 | Define a protocol behaviour. 97 | """ 98 | defmacro defprotocolEx(name, opts \\ [], [do: body]) do 99 | # body = globalize_ast(body, __CALLER__, __MODULE__.ProtoScope) 100 | parsed_name = get_atom_name(name, __CALLER__) 101 | name = get_atom_name(name) 102 | desc_name = get_atom_name_with(name, @desc_name) |> get_atom_name(__CALLER__) 103 | as = 104 | case opts[:as] do 105 | nil -> nil 106 | {name, _, scope} when is_atom(name) and is_atom(scope) -> name 107 | end 108 | body = 109 | case body do 110 | {:__block__, _meta, _lines} = ast -> ast 111 | line -> {:__block__, [], [line]} 112 | end 113 | |> case do {:__block__, meta, lines} -> 114 | lines = Enum.map(lines, fn 115 | {type, _, _} = ast when type in [:def, :defp, :defmacro, :defmacrop, :@] -> ast 116 | ast -> Macro.expand(ast, __CALLER__) 117 | end) 118 | {:__block__, meta, lines} 119 | end 120 | spec = decompose_spec(__CALLER__, as, body) 121 | spec = verify_valid_spec(spec) 122 | desc_body = 123 | quote do 124 | @moduledoc false 125 | 126 | Module.register_attribute(__MODULE__, unquote(@desc_attr), persist: true) 127 | @protocol_ex_desc unquote(parsed_name) 128 | def spec, do: unquote(Macro.escape(spec)) 129 | end 130 | # desc_body |> Macro.to_string() |> Code.format_string!() |> IO.puts() 131 | Module.create(desc_name, desc_body, __CALLER__) # Macro.Env.location(__CALLER__)) 132 | consolidate(parsed_name, [impls: []]) # A temporary hoister 133 | if parsed_name == name do 134 | generate_alias_usage(body, __CALLER__) 135 | else 136 | [ 137 | quote(generated: true, do: alias(unquote(parsed_name), as: unquote(name))), 138 | quote(generated: true, do: _ = unquote(name)) 139 | | generate_alias_usage(body, __CALLER__) 140 | ] 141 | end 142 | end 143 | 144 | defmacro defprotocol_ex(name, opts \\ [], bodies) do 145 | quote do 146 | ProtocolEx.defprotocolEx(unquote(name), unquote(opts), unquote(bodies)) 147 | end 148 | end 149 | 150 | 151 | 152 | @doc """ 153 | Implement a protocol based on a matcher specification 154 | """ 155 | defmacro defimplEx(impl_name, matcher, [{:for, for_name} | opts], [do: body]) do 156 | name = globalize_ast(for_name, __CALLER__, __MODULE__.Unused) 157 | name = get_atom_name(name) 158 | name = __CALLER__.aliases[name] || name 159 | desc_name = get_desc_name(name) 160 | impl_name = get_atom_name(impl_name) 161 | # body = globalize_ast(body, __CALLER__, __MODULE__.ImplScope) 162 | gmatcher = globalize_ast(matcher, __CALLER__, __MODULE__.ImplScope) 163 | [ quote do 164 | require unquote(desc_name) 165 | ProtocolEx.defimplEx_do(unquote(Macro.escape(impl_name)), unquote(Macro.escape(gmatcher)), [for: unquote(Macro.escape(name))], [do: unquote(Macro.escape(body))], unquote(opts), unquote({:__ENV__, [], nil})) 166 | end 167 | | generate_alias_usage(matcher, __CALLER__) 168 | ++ generate_alias_usage(body, __CALLER__) 169 | ++ generate_alias_usage(for_name, __CALLER__) 170 | ] # |>case do ast -> IO.puts(Code.format_string!(Macro.to_string(ast))); ast end 171 | end 172 | 173 | defmacro defimpl_ex(impl_name, matcher, opts, bodies) do 174 | quote do 175 | ProtocolEx.defimplEx(unquote(impl_name), unquote(matcher), unquote(opts), unquote(bodies)) 176 | end 177 | end 178 | 179 | @doc false 180 | def defimplEx_do(impl_name, matcher, [for: name], [do: body], opts, caller_env) do 181 | desc_name = get_desc_name(name) 182 | impl_name = get_atom_name(impl_name, caller_env) 183 | impl_name = get_impl_name(name, impl_name) 184 | impl_name = get_atom_name(impl_name, caller_env) 185 | spec = desc_name.spec() 186 | 187 | test_asts = gen_impl_test_asts(spec) 188 | 189 | impl_quoted = {:__block__, [], 190 | [ if spec.location[:file] in [nil, "", ~C""] do 191 | :no_file_resource 192 | else 193 | quote do 194 | # @external_resource unquote(spec.location[:file]) 195 | require unquote(desc_name) 196 | end 197 | end, 198 | quote do 199 | def __matcher__, do: [unquote(Macro.escape(matcher))] 200 | def __spec__, do: unquote(desc_name).spec() 201 | Module.register_attribute(__MODULE__, :protocol_ex, persist: true) 202 | @protocol_ex unquote(name) 203 | Module.register_attribute(__MODULE__, :priority, persist: true) 204 | end 205 | ] ++ 206 | (case opts[:inline] do 207 | nil -> [quote do def __inlined__(_), do: nil end] 208 | # :all -> quote do def __inlined__(_), do: true end 209 | funs when is_list(funs) -> 210 | funs 211 | |> Enum.map(fn {fun, arity} -> 212 | Macro.prewalk(body, {[], false}, fn 213 | ({:def, _, [{^fun, _, bindings}, _]} = ast, {acc, false}) when length(bindings) === arity -> 214 | {ast, {[ast | acc], false}} 215 | ({:def, _, [{:when, _, [{^fun, _, bindings}, _]}, _]} = ast, {acc, false}) when length(bindings) === arity -> 216 | {ast, {[ast | acc], false}} 217 | ({:@, _, [{:ignore, _, _}]}, {acc, false}) -> 218 | {nil, {acc, true}} 219 | (ast, {acc, _ignore_next}) -> 220 | {ast, {acc, false}} 221 | end) 222 | |> case do 223 | {_body, {ast, _ignore_next}} -> 224 | quote do def __inlined__({unquote(fun), unquote(arity)}), do: unquote(Macro.escape(ast)) end 225 | end 226 | end) 227 | |> List.wrap() 228 | |> Enum.reverse([quote do def __inlined__(_), do: nil end]) 229 | end 230 | |> case do 231 | old_inlines -> 232 | Macro.prewalk(body, {%{}, false}, fn 233 | ({:def, _, [{fun, _, bindings}, _]} = ast, {acc, inline}) when is_list(bindings) -> 234 | arity = length(bindings) 235 | if inline or Map.get(acc, {fun, arity}, false) do 236 | acc = Map.update(acc, {fun, arity}, [ast], &[ast | List.wrap(&1)]) 237 | {ast, {acc, false}} 238 | else 239 | {ast, {acc, false}} 240 | end 241 | ({:def, _, [{:when, _, [{fun, _, bindings}, _]}, _]} = ast, {acc, inline}) when is_list(bindings)-> 242 | arity = length(bindings) 243 | if inline or Map.get(acc, {fun, arity}, false) do 244 | acc = Map.update(acc, {fun, arity}, [ast], &[ast | List.wrap(&1)]) 245 | {ast, {acc, false}} 246 | else 247 | {ast, {acc, false}} 248 | end 249 | ({:@, _, [{:inline, _, _}]}, {acc, false}) -> {nil, {acc, true}} 250 | (ast, {acc, _inline_next}) -> {ast, {acc, false}} 251 | end) 252 | |> case do 253 | {body, {asts, _inline_next}} -> 254 | Enum.map(asts, fn {{fun, arity}, ast} -> 255 | quote do def __inlined__({unquote(fun), unquote(arity)}), do: unquote(Macro.escape(ast)) end 256 | end) ++ 257 | old_inlines ++ 258 | List.wrap(body) ++ 259 | test_asts 260 | end 261 | end) 262 | } 263 | # impl_quoted |> Macro.to_string() |> IO.puts 264 | if Code.ensure_loaded?(impl_name) do 265 | :code.purge(impl_name) 266 | end 267 | Module.create(impl_name, impl_quoted, caller_env) # Macro.Env.location(caller_env)) 268 | verify_valid_spec_on_module(name, spec, impl_name) 269 | end 270 | 271 | 272 | 273 | def consolidate_all(opts \\ []) do 274 | opts 275 | |> get_base_paths() 276 | |> Enum.flat_map(fn path -> 277 | path 278 | |> Path.join("*.beam") 279 | |> Path.wildcard() 280 | end) 281 | |> case do beam_paths -> 282 | beam_paths 283 | |> Enum.flat_map(fn path -> # Oh what I'd give for a monadic `do` right about now... >.> 284 | case :beam_lib.chunks(path |> to_charlist(), [:attributes]) do 285 | {:ok, {_mod, chunks}} -> 286 | case get_in(chunks, [:attributes, @desc_attr]) do 287 | [proto] -> [proto] 288 | _ -> [] 289 | end 290 | _err -> [] 291 | end 292 | end) 293 | |> case do protocols -> 294 | beam_paths 295 | |> Enum.flat_map(fn path -> 296 | case :beam_lib.chunks(path |> to_charlist(), [:attributes]) do 297 | {:ok, {mod, chunks}} -> 298 | attributes = chunks[:attributes] 299 | case attributes[:protocol_ex] do 300 | nil -> [] 301 | protos -> 302 | protos 303 | |> Enum.any?(&Enum.member?(protocols, &1)) 304 | |> if do 305 | priority = hd(attributes[:priority] || [0]) 306 | data = {mod, priority, protos} 307 | [data] 308 | else 309 | [] 310 | end 311 | end 312 | _err -> [] 313 | end 314 | end) 315 | |> case do impls -> 316 | # IO.inspect {:consolidating_all, protocols} 317 | protocols 318 | |> Enum.map(fn proto_name -> 319 | consolidate(proto_name, [impls: impls, output_beam: opts[:output_beam], verbose: opts[:verbose], print_protocol_ex: opts[:print_protocol_ex], protocol_tests: opts[:protocol_tests]]) 320 | end) 321 | end 322 | end 323 | end 324 | end 325 | 326 | @doc """ 327 | Resolve a protocol into a final ready-to-use-module based on already-compiled names sorted by priority 328 | """ 329 | def consolidate(proto_name, opts \\ []) do 330 | impls = 331 | case opts[:impls] do 332 | nil -> 333 | opts 334 | |> get_base_paths() 335 | |> Enum.flat_map(fn path -> 336 | path 337 | |> Path.join("*.beam") 338 | |> Path.wildcard() 339 | end) 340 | |> Enum.flat_map(fn path -> 341 | case :beam_lib.chunks(path |> to_charlist(), [:attributes]) do 342 | {:ok, {mod, chunks}} -> 343 | attributes = chunks[:attributes] 344 | case attributes[:protocol_ex] do 345 | nil -> [] 346 | protos -> 347 | if Enum.member?(protos, proto_name) do 348 | priority = hd(attributes[:priority] || [0]) 349 | data = {mod, priority, protos} 350 | [data] 351 | else 352 | [] 353 | end 354 | end 355 | _err -> [] 356 | end 357 | end) 358 | impls -> Enum.filter(impls, &(Enum.member?(elem(&1, 2), proto_name))) 359 | end 360 | |> Enum.sort_by(fn {name, prio, _} -> {prio, to_string(name)} end, &>=/2) # Sort by priority, then by binary name 361 | |> Enum.map(&elem(&1, 0)) 362 | 363 | if :erlang.function_exported(proto_name, :__proto_ex_consolidated__, 0) and 364 | :erlang.function_exported(proto_name, :__proto_ex_impls__, 0) and 365 | # proto_name.__proto_ex_consolidated__() and 366 | proto_name.__proto_ex_impls__() === impls do 367 | proto_name 368 | else 369 | # IO.inspect {:consolidate, proto_name, impls, if(:erlang.function_exported(proto_name, :__proto_ex_impls__, 0), do: proto_name.__proto_ex_impls__(), else: :does_not_exist)} 370 | proto_desc = Module.concat(proto_name, @desc_name) 371 | spec = 372 | case proto_desc.spec() do 373 | %Spec{} = spec -> spec 374 | err -> throw {:invalid_spec, err} 375 | end 376 | 377 | Enum.map(List.wrap(spec.cache[:requiring]), fn module -> 378 | if not Code.ensure_loaded?(module) or 379 | not :erlang.function_exported(module, :__proto_ex_consolidated__, 0) or 380 | not module.__proto_ex_consolidated__() do 381 | consolidate(module, [output_beam: opts[:output_beam]]) 382 | end 383 | end) 384 | 385 | impl_quoted = {:__block__, [], 386 | if(spec.docs[:moduledoc], do: spec.docs[:moduledoc], else: [quote(do: @moduledoc "")]) ++ 387 | Enum.map(impls, "e(do: require unquote(&1))) ++ 388 | :lists.reverse(spec.head_asts) ++ 389 | [ quote do def __protocol_ex__, do: unquote(Macro.escape(clean_spec(spec))) end, 390 | quote do def __proto_ex_consolidated__, do: unquote(if(impls === [], do: false, else: true)) end, 391 | quote do def __proto_ex_impls__, do: unquote(impls) end 392 | | Enum.flat_map(:lists.reverse(spec.callbacks), &load_abstract_from_impls(spec, proto_name, &1, impls)) 393 | ] ++ 394 | Enum.flat_map(spec.callbacks, &load_test_from_impls(proto_name, &1, impls)) ++ 395 | load_tests_from_impls(spec.callbacks) 396 | } 397 | # impl_quoted |> Macro.to_string() |> IO.puts 398 | if Code.ensure_loaded?(proto_name) do 399 | # IO.inspect {:purging, proto_name} 400 | :code.purge(proto_name) 401 | end 402 | Code.compiler_options(ignore_module_conflict: true) 403 | if opts[:print_protocol_ex] || System.get_env("PRINT_PROTOCOL_EX") not in [nil, ""] do 404 | quote do 405 | defmodule unquote(proto_name) do 406 | unquote(impl_quoted) 407 | end 408 | end 409 | |> Macro.to_string() 410 | |> Code.format_string!() 411 | |> IO.puts() 412 | end 413 | #IO.inspect({proto_name, impl_quoted, spec.location}, label: :CONSOLIDATED) 414 | #impl_quoted |> Macro.to_string() |> Code.format_string!() |> IO.puts() 415 | {:module, beam_name, beam_data, _exports} = Module.create(proto_name, impl_quoted, spec.location) 416 | case opts[:output_beam] do 417 | base_path when is_binary(base_path) -> base_path 418 | true -> apply(Mix.Project, :build_path, []) 419 | _ -> nil 420 | end 421 | |> case do 422 | nil -> if(opts[:verbose], do: IO.puts("ProtocolEx inline module: #{beam_name}")) 423 | base_path -> 424 | beam_filename = "#{beam_name}.beam" 425 | glob = Path.join([base_path, "lib", "**", beam_filename]) 426 | path = 427 | glob 428 | |> Path.wildcard() 429 | |> Enum.sort() 430 | |> case do 431 | [] -> raise "ProtocolEx consolidation failed: could not find anything for `#{glob}`" 432 | [path] -> path 433 | [_|_] = paths -> raise "ProtocolEx consolidation failed: found multiple beam files for `#{glob}` of: #{inspect paths}" 434 | end 435 | File.write!(path, beam_data) 436 | if(opts[:verbose], do: IO.puts("ProtocolEx beam module #{beam_filename} with implementations #{inspect impls}")) 437 | end 438 | Code.compiler_options(ignore_module_conflict: false) 439 | if(opts[:protocol_tests], do: proto_name.__tests_pex__(opts[:protocol_tests])) 440 | if(false && opts[:verbose], do: IO.puts("ProtocolEx Consolidated: #{proto_name}")) 441 | proto_name 442 | end 443 | end 444 | 445 | @doc """ 446 | Resolve a protocol into a final ready-to-use-module based on implementation names 447 | """ 448 | defmacro resolveProtocolEx(orig_name, impls, priority_sorted \\ false) when is_list(impls) do 449 | if(priority_sorted, do: IO.puts("`priority_sorted` is no longer supported, internal sorting is now always used")) 450 | name = get_atom_name(orig_name) 451 | name = __CALLER__.aliases[name] || name 452 | desc_name = get_desc_name(name) 453 | impls = Enum.map(impls, &get_atom_name/1) 454 | impls = Enum.map(impls, &get_atom_name_with(name, &1)) 455 | impls = Enum.map(impls, &get_atom_name/1) 456 | impls = Enum.map(impls, &{&1, &1.module_info()[:attributes][:priority] || 0, [name]}) 457 | #impls = if(priority_sorted, do: Enum.sort_by(impls, &(&1.module_info()[:attributes][:priority] || 0), &>=/2), else: impls) 458 | requireds = Enum.map([desc_name | impls], fn 459 | {req, _, _} -> {:require, [], [req]} 460 | req -> {:require, [], [req]} 461 | end) 462 | quote do 463 | __silence_alias_warnings__ = unquote(orig_name) 464 | unquote_splicing(requireds) 465 | ProtocolEx.resolveProtocolEx_do(unquote(name), unquote(impls)) 466 | end 467 | end 468 | 469 | defmacro resolve_protocol_ex(orig_name, impls, priority_sorted \\ false) when is_list(impls) do 470 | quote do 471 | ProtocolEx.resolveProtocolEx(unquote(orig_name), unquote(impls), unquote(priority_sorted)) 472 | end 473 | end 474 | 475 | @doc false 476 | defmacro resolveProtocolEx_do(name, impls) when is_list(impls) do 477 | consolidate(name, impls: impls) 478 | if false do 479 | name = get_atom_name(name) 480 | desc_name = get_desc_name(name) 481 | spec = desc_name.spec() 482 | impl_quoted = {:__block__, [], 483 | Enum.map(impls, "e(do: require unquote(&1))) ++ 484 | :lists.reverse(spec.head_asts) ++ 485 | [ quote do def __protocol_ex__, do: unquote(Macro.escape(clean_spec(spec))) end, 486 | quote do def __proto_ex_consolidated__, do: unquote(if(impls === [], do: false, else: true)) end, 487 | quote do def __proto_ex_impls__, do: unquote(impls) end 488 | ] ++ 489 | Enum.flat_map(:lists.reverse(spec.callbacks), &load_abstract_from_impls(spec, name, &1, impls)) ++ 490 | Enum.flat_map(spec.callbacks, &load_test_from_impls(name, &1, impls)) ++ 491 | load_tests_from_impls(spec.callbacks) 492 | } 493 | # impl_quoted |> Macro.to_string() |> IO.puts 494 | if Code.ensure_loaded?(name) do 495 | :code.purge(name) 496 | end 497 | Code.compiler_options(ignore_module_conflict: true) 498 | #impl_quoted|> Macro.to_string()|>Code.format_string!()|>IO.puts() 499 | #impl_quoted|>IO.inspect() 500 | Module.create(name, impl_quoted, spec.location) 501 | Code.compiler_options(ignore_module_conflict: false) 502 | if(true, do: name.__tests_pex__([])) # TODO: Make this configurable 503 | end 504 | :ok 505 | end 506 | 507 | 508 | defp get_base_paths(opts) do 509 | case opts[:ebin_root] do 510 | nil -> :code.get_path() 511 | paths -> List.wrap(paths) 512 | end 513 | end 514 | 515 | 516 | defp globalize_ast(ast, env, scope) do 517 | Macro.prewalk(ast, fn 518 | {binding, ctx, nil} -> {binding, ctx, scope} 519 | {:__aliases__, _ctx, [arg | args]} = ast -> 520 | mod = Module.concat([arg]) 521 | case env.aliases[mod] do 522 | nil -> ast 523 | mod -> Module.concat([mod | args]) 524 | end 525 | ast -> ast 526 | end) 527 | end 528 | 529 | 530 | defp get_atom_name(name, env \\ %{module: nil}) 531 | defp get_atom_name(name, env) when is_atom(name), do: Module.concat(get_env_name(env)++[name]) 532 | defp get_atom_name({:__aliases__, _, names}, env) when is_list(names), do: Module.concat(get_env_name(env)++names) 533 | 534 | defp get_env_name(%{module: nil}), do: [] 535 | defp get_env_name(%{module: module}), do: [module] 536 | 537 | defp get_atom_name_with(name, at_end) when is_atom(name) and is_atom(at_end), do: {:__aliases__, [alias: false], [name, at_end]} 538 | defp get_atom_name_with({:__aliases__, meta, names}, at_end) when is_list(names) and is_atom(at_end), do: {:__aliases__, meta, names ++ [at_end]} 539 | 540 | defp get_desc_name(name) when is_atom(name), do: Module.concat([name, @desc_name]) 541 | 542 | defp get_impl_name(name, impl_name) when is_atom(name), do: Module.concat(name, impl_name) 543 | 544 | 545 | defp decompose_spec(env, as, body), do: decompose_spec(env, as, %Spec{as: as, location: Macro.Env.location(env)}, body) 546 | defp decompose_spec(env, as, returned, {:__block__, _, body}), do: decompose_spec(env, as, returned, body) 547 | defp decompose_spec(_env, _as, returned, []), do: returned 548 | defp decompose_spec(env, as, returned, [elem | rest]), do: decompose_spec(env, as, decompose_spec_element(env, as, returned, elem), rest) 549 | defp decompose_spec(env, as, returned, body), do: decompose_spec(env, as, returned, [body]) 550 | 551 | 552 | defp decompose_spec_element(env, as, returned, elem) 553 | # defp decompose_spec_element(returned, {:def, meta, [{name, name_meta, noargs}]}) when is_atom(noargs), do: decompose_spec_element(returned, {:def, meta, [{name, name_meta, []}]}) 554 | defp decompose_spec_element(_env, as, returned, {:def, _meta, [head]} = elem) do 555 | {name, args_length, defaults} = decompose_spec_head(as, head) 556 | elem = Macro.prewalk(elem, fn {:\\, _, [b, _]} -> b; a -> a end) 557 | callbacks = [{name, args_length, elem}] ++ defaults ++ returned.callbacks 558 | doc = List.wrap(returned.cache[:doc]) 559 | %{returned | 560 | callbacks: callbacks, 561 | docs: if(doc === [], do: returned.docs, else: Map.put(returned.docs, {name, args_length}, doc)), 562 | cache: Map.put(returned.cache, :doc, []) 563 | } 564 | end 565 | defp decompose_spec_element(_env, as, returned, {:def, meta, [head, body]}) do 566 | {name, args_length, defaults} = decompose_spec_head(as, head) 567 | head = Macro.prewalk(head, fn {:\\, _, [b, _]} -> b; a -> a end) 568 | elem = {:def, meta, [head, body]} 569 | head = {:def, meta, [head]} 570 | callbacks = [{name, args_length, head, elem}] ++ defaults ++ returned.callbacks 571 | doc = List.wrap(returned.cache[:doc]) 572 | %{returned | 573 | callbacks: callbacks, 574 | docs: if(doc === [], do: returned.docs, else: Map.put(returned.docs, {name, args_length}, doc)), 575 | cache: Map.put(returned.cache, :doc, []) 576 | } 577 | end 578 | defp decompose_spec_element(_env, _as, returned, {:deftest, meta, [{name, _, scope}, checks]}) when is_atom(scope) do 579 | callbacks = [{:extra, :test, name, meta, checks} | returned.callbacks] 580 | doc = List.wrap(returned.cache[:doc]) 581 | %{returned | 582 | callbacks: callbacks, 583 | docs: if(doc === [], do: returned.docs, else: Map.put(returned.docs, name, doc)), 584 | cache: Map.put(returned.cache, :doc, []) 585 | } 586 | end 587 | defp decompose_spec_element(_env, _as, returned, {pt, _meta, _body} = ast) when pt in [ 588 | :defmacro, :defmacrop, :defp, 589 | :spec, :type, :opaque, 590 | :moduledoc, 591 | ] do 592 | %{returned | head_asts: [ast | returned.head_asts]} 593 | end 594 | defp decompose_spec_element(_env, _as, returned, {:@, _meta, [{:doc, _doc_meta, _doc_args}]} = doc_ast) do 595 | %{returned | cache: Map.update(returned.cache, :doc, [doc_ast], &[doc_ast | &1])} 596 | end 597 | defp decompose_spec_element(_env, _as, returned, {:@, _meta, [{:moduledoc, _mdoc_meta, _mdoc_args}]} = mdoc_ast) do 598 | %{returned | docs: Map.update(returned.docs, :moduledoc, [mdoc_ast], &[mdoc_ast | &1])} 599 | end 600 | defp decompose_spec_element(env, _as, returned, {:@, _meta, [{:extends, _doc_meta, extending}]}) do 601 | extending = Enum.map(extending, fn 602 | {:__aliases__, _meta, names} -> 603 | m = Module.concat(names) 604 | env.aliases[m] || m 605 | name when is_atom(name) -> name 606 | end) 607 | %{returned | cache: Map.put(returned.cache, :extending, extending ++ List.wrap(returned.cache[:extending]))} 608 | end 609 | defp decompose_spec_element(env, _as, returned, {:require, _, requiring}) do 610 | requiring = Enum.map(requiring, fn 611 | {:__aliases__, _meta, names} -> 612 | m = Module.concat(names) 613 | env.aliases[m] || m 614 | name when is_atom(name) -> name 615 | end) 616 | %{returned | cache: Map.put(returned.cache, :requiring, requiring ++ List.wrap(returned.cache[:requiring]))} 617 | end 618 | defp decompose_spec_element(_env, _as, _returned, unhandled_elem), do: raise %InvalidProtocolSpecification{ast: unhandled_elem} 619 | 620 | 621 | defp decompose_spec_head(as, head) 622 | defp decompose_spec_head(as, {:when, _when_meta, [head, _guard]}) do 623 | decompose_spec_head(as, head) 624 | end 625 | defp decompose_spec_head(_as, {name, ctx, args} = _head) when is_atom(name) and is_list(args) do 626 | # if as != nil and args != [] do 627 | # Enum.find(args, false, fn 628 | # {^as, _, scope} when is_atom(scope) -> true 629 | # _ -> false 630 | # end) || raise %MissingAtInArgs{as: as, ast: head} 631 | # end 632 | defaults = generate_default_functions(name, ctx, args) 633 | {name, length(args), defaults} 634 | end 635 | defp decompose_spec_head(_as, head), do: raise %InvalidProtocolSpecification{ast: head} 636 | 637 | defp generate_default_functions(name, ctx, args_so_far \\ [], args, acc \\ []) 638 | defp generate_default_functions(_name, _ctx, _args_so_far, [], acc), do: acc 639 | defp generate_default_functions(name, ctx, args_so_far, [{:\\, _dctx, [_binding, default_ast]} | args], acc) do 640 | args_proc = Enum.filter(args, fn {:\\, _, _} -> false; _ -> true end) 641 | def_args = args_so_far ++ args_proc 642 | args_call = args_so_far ++ [default_ast] ++ args_proc 643 | args_so_far = args_so_far ++ [default_ast] 644 | def_head = {name, ctx, def_args} 645 | def_ast = {:def, ctx, [def_head, [do: {name, ctx, args_call}]]} 646 | default = {name, length(def_args), def_head, def_ast} 647 | generate_default_functions(name, ctx, args_so_far, args, [default | acc]) 648 | end 649 | defp generate_default_functions(name, ctx, args_so_far, [arg | args], acc) do 650 | args_so_far = args_so_far ++ [arg] 651 | generate_default_functions(name, ctx, args_so_far, args, acc) 652 | end 653 | 654 | 655 | defp verify_valid_spec(spec) do 656 | # Sort callbacks 657 | spec_callbacks = Enum.sort(spec.callbacks, &>/2) 658 | # Verify only valid definitions 659 | callbacks = Enum.uniq_by(spec_callbacks, fn # The if's are to verify no 0-arity and 1-arity at same time 660 | {name, arity, _elem} -> {name, if(arity===0, do: 1, else: arity)} 661 | {name, arity, _elem_head, _elem} -> {name, if(arity===0, do: 1, else: arity)} 662 | {:extra, :test, name, _meta, _checks} -> {:extra, :test, name} 663 | end) 664 | # Verify no duplicate callback spec 665 | if length(spec_callbacks) !== length(callbacks) do 666 | [{name, arity, _elem}|_] = spec_callbacks -- callbacks 667 | case arity do 668 | 0 -> raise %DuplicateSpecification{name: name, arity: -1} 669 | _ -> raise %DuplicateSpecification{name: name, arity: arity} 670 | end 671 | end 672 | %{spec | callbacks: callbacks} 673 | end 674 | 675 | 676 | defp verify_valid_spec_on_module(proto, spec, module) do 677 | spec.callbacks 678 | |> Enum.map(fn 679 | {name, arity, _} -> 680 | if :erlang.function_exported(module, name, arity) do 681 | :ok 682 | else 683 | try do 684 | mname = String.to_existing_atom("MACRO-#{name}") 685 | marity = arity + 1 686 | if :erlang.function_exported(module, mname, marity) do 687 | :ok 688 | else 689 | raise %MissingRequiredProtocolDefinition{proto: proto, impl: module, name: name, arity: arity} 690 | end 691 | rescue ArgumentError -> 692 | raise %MissingRequiredProtocolDefinition{proto: proto, impl: module, name: name, arity: arity} 693 | end 694 | end 695 | {_name, _arity, _, _} -> :ok 696 | {:extra, :test, _name, _meta, _checks} -> :ok 697 | end) 698 | :ok 699 | end 700 | 701 | 702 | defp gen_impl_test_asts(spec) do 703 | opts = Macro.var(:opts, nil) 704 | Enum.filter(spec.callbacks, fn 705 | {:extra, :test, _name, _meta, _checks} -> true 706 | _ -> false 707 | end) 708 | |> case do 709 | [] -> [] 710 | tests -> 711 | Enum.map(tests, fn {:extra, :test, name, _meta, body} -> 712 | quote do 713 | def __tests_pex__(unquote(name), unquote(opts)) do 714 | _ = unquote(opts) 715 | unquote_splicing(List.wrap(body[:do])) 716 | end 717 | end 718 | end) 719 | end 720 | end 721 | 722 | 723 | def load_tests_from_impls(callbacks) do 724 | opts = Macro.var(:opts, __MODULE__) 725 | tests = Enum.flat_map(callbacks, fn 726 | {:extra, :test, name, _meta, _checks} -> 727 | [quote do 728 | __tests_pex__(unquote(name), unquote(opts)) 729 | end] 730 | _ -> [] 731 | end) 732 | [quote do 733 | def __tests_pex__(unquote(opts)) do 734 | unquote_splicing(tests) 735 | end 736 | end] 737 | end 738 | 739 | 740 | defp load_test_from_impls(proto, {:extra, :test, name, meta, _checks}, impls) do 741 | opts = Macro.var(:opts, __MODULE__) 742 | [quote do 743 | def __tests_pex__(unquote(name), unquote(opts)) do 744 | unquote_splicing(Enum.map(impls, fn impl -> 745 | quote do 746 | try do 747 | unquote(impl).__tests_pex__(unquote(name), unquote(opts)) 748 | rescue 749 | ProtocolEx.UnimplementedProtocolEx -> :ok # Handling unimplemented specifically to allow overrides 750 | exc -> exc 751 | catch err -> err 752 | end 753 | |> case do 754 | {:ok, _value} -> :ok 755 | {:error, err_data} -> raise %ProtocolExTestFailure{ 756 | proto: unquote(proto), 757 | type: unquote(impl), 758 | name: unquote(name), 759 | meta: unquote(meta), 760 | value: err_data, 761 | } 762 | :ok -> :ok 763 | true -> :ok 764 | nil -> :ok 765 | err_data -> raise %ProtocolExTestFailure{ 766 | proto: unquote(proto), 767 | type: unquote(impl), 768 | name: unquote(name), 769 | meta: unquote(meta), 770 | value: err_data, 771 | } 772 | end 773 | end 774 | end)) 775 | end 776 | end] 777 | end 778 | defp load_test_from_impls(_proto, _abstract, _impls), do: [] 779 | 780 | 781 | defp load_abstract_from_impls(spec, pname, abstract, impls, returning \\ []) 782 | defp load_abstract_from_impls(spec, pname, abstract, impls, []) do 783 | doc_key = 784 | case abstract do 785 | {name, arity, _ast_head} -> {name, arity} 786 | {name, arity, _ast_head, _ast_fallback} -> {name, arity} 787 | {:extra, :test, name, _meta, _checks} -> name 788 | end 789 | doc = 790 | case spec.docs[doc_key] do 791 | nil -> [quote do @doc "" end] 792 | ast -> ast 793 | end 794 | load_abstract_from_impls(spec, pname, abstract, impls, doc) 795 | end 796 | defp load_abstract_from_impls(spec, pname, abstract, [], returning) do 797 | case abstract do 798 | {name, 0, {def, meta, [{name, name_meta, []}]} = ast_head} -> 799 | body = 800 | {:raise, [context: Elixir, import: Kernel], 801 | [{:%, [], 802 | [{:__aliases__, [alias: false], [:ProtocolEx, :UnimplementedProtocolEx]}, 803 | {:%{}, [], 804 | [proto: pname, name: name, arity: 0, value: []] 805 | }]}]} 806 | catch_all = append_body_to_head(ast_head, body) 807 | doc = quote do @doc unquote("See `#{name}/1`") end 808 | returning = returning ++ [catch_all, doc] 809 | arg = Macro.var(spec.as || :unused, __MODULE__) 810 | ast_head = {def, [generated: true] ++ meta, [{name, name_meta, [arg]}]} 811 | load_abstract_from_impls(spec, pname, {name, 1, ast_head}, [], returning) 812 | {name, arity, ast_head} -> 813 | args = Macro.generate_arguments(length(get_args_from_head(ast_head)), __MODULE__) 814 | head_args = Enum.map(args, &{:=, [generated: true], [@no_match, &1]}) 815 | ast_head = replace_head_args_with(ast_head, head_args) 816 | ast_head = remove_guards(ast_head) 817 | body = 818 | {:raise, [context: Elixir, import: Kernel], 819 | [{:%, [], 820 | [{:__aliases__, [alias: false], [:ProtocolEx, :UnimplementedProtocolEx]}, 821 | {:%{}, [], 822 | [proto: pname, name: name, arity: arity, value: args] 823 | }]}]} 824 | {c, cmeta, a} = append_body_to_head(ast_head, body) 825 | catch_all = {c, [generated: true] ++ cmeta, a} 826 | :lists.reverse(returning, [catch_all]) 827 | {name, 0, _ast_head, {def, meta, [{name, name_meta, []}, _body]} = ast_fallback} -> 828 | arg = Macro.var(spec.as || :unused, __MODULE__) 829 | ast_bounce = {def, [generated: true] ++ meta, [{name, name_meta, [arg]}, [do: quote do 830 | _ = unquote(arg) 831 | unquote(name)() 832 | end]]} 833 | doc = quote do @doc unquote("See `#{name}/1`") end 834 | :lists.reverse([ast_bounce | returning], [doc, ast_fallback]) 835 | {_name, _arity, ast_head, {:def, meta, def_ast}} -> 836 | {_ast_head, head_args} = 837 | Macro.prewalk(ast_head, [], fn 838 | {name, _meta, scope} = arg, acc when is_atom(name) and is_atom(scope) -> 839 | {arg, [arg | acc]} 840 | ast, acc -> 841 | {ast, acc} 842 | end) 843 | head_args = 844 | head_args 845 | |> Enum.uniq() 846 | |> Enum.map("e(do: _ = unquote(&1))) 847 | [body | def_ast] = :lists.reverse(def_ast) 848 | body = Keyword.put(body, :do, quote(do: (unquote_splicing(head_args);unquote(body[:do])))) 849 | def_ast = :lists.reverse(def_ast, [body]) 850 | ast_fallback = {:def, meta, def_ast} 851 | :lists.reverse(returning, [ast_fallback]) 852 | {:extra, :test, _name, _meta, _checks} -> [] 853 | end 854 | end 855 | defp load_abstract_from_impls(spec, pname, abstract, [impl | impls], returning) do 856 | case abstract do 857 | {name, arity, ast_head} -> 858 | mname = String.to_atom("MACRO-#{name}") 859 | marity = arity + 1 860 | if Enum.any?(impl.module_info()[:exports], fn 861 | {^name, ^arity} -> true; 862 | {^mname, ^marity} -> true; 863 | _ -> false end) do 864 | {name, ast_head} 865 | else 866 | raise %MissingRequiredProtocolDefinition{proto: pname, impl: impl, name: name, arity: arity} 867 | end 868 | {name, arity, ast_head, _ast_fallback} -> 869 | mname = String.to_atom("MACRO-#{name}") 870 | marity = arity + 1 871 | if Enum.any?(impl.module_info()[:exports], fn 872 | {^name, ^arity} -> true; 873 | {^mname, ^marity} -> true; 874 | _ -> false end) do 875 | {name, ast_head} 876 | else 877 | :skip 878 | end 879 | {:extra, :test, _name, _meta, _checks} = test -> test 880 | end 881 | |> case do 882 | :skip -> load_abstract_from_impls(spec, pname, abstract, impls, returning) 883 | {name, ast_head} -> 884 | matchers = List.wrap(impl.__matcher__()) 885 | args = get_args_from_head(ast_head) 886 | arity = length(args) 887 | if :erlang.function_exported(impl, :__inlined__, 1) do 888 | impl.__inlined__({name, arity}) 889 | else 890 | nil 891 | end 892 | |> case do 893 | nil -> 894 | body = build_body_call_with_args(impl, name, args) 895 | head_args = 896 | case bind_matcher_to_args(spec.as, matchers, args) do 897 | [] -> bind_matcher_to_args(spec.as, matchers, [Macro.var(:_, __MODULE__)]) # 0-arity to 1-arity of the matcher 898 | head_args -> head_args 899 | end 900 | ast_head = replace_head_args_with(ast_head, head_args) 901 | guard = get_guards_from_matchers(matchers) 902 | ast_head = add_guard_to_head(ast_head, guard) 903 | ast = append_body_to_head(ast_head, body) 904 | load_abstract_from_impls(spec, pname, abstract, impls, [ast | returning]) 905 | inlined_impls when is_list(inlined_impls) -> 906 | guard = get_guards_from_matchers(matchers) 907 | inlined_impls = 908 | if(guard == true) do # TODO: Maybe change this to force guard on inlined heads? Probably not, maybe only on plain variables? 909 | Enum.map(inlined_impls, &add_guard_to_head(&1, guard)) 910 | else 911 | inlined_impls 912 | end 913 | load_abstract_from_impls(spec, pname, abstract, impls, inlined_impls ++ returning) 914 | end 915 | {:extra, :test, _name, _meta, _checks} -> 916 | returning = [{impl} | returning] 917 | load_abstract_from_impls(spec, pname, abstract, impls, returning) 918 | end 919 | end 920 | 921 | defp get_args_from_head(ast_head) 922 | defp get_args_from_head({:def, _meta, [{:when, _when_meta, [{_name, _name_meta, args}, _guard]}]}) do 923 | args 924 | end 925 | defp get_args_from_head({:def, _meta, [{_name, _name_meta, args}]}) do 926 | args 927 | end 928 | 929 | defp bind_matcher_to_args(as, matcher, args, returned \\ []) 930 | defp bind_matcher_to_args(nil, _matcher, [], returned), do: :lists.reverse(returned) 931 | defp bind_matcher_to_args(nil, [], args, returned), do: :lists.reverse(returned, args) 932 | defp bind_matcher_to_args(nil, [{:when, _when_meta, [binding_ast, _when_call]} | matchers], [arg_ast | args], returned) do 933 | arg = {:=, [], [binding_ast, arg_ast]} 934 | bind_matcher_to_args(nil, matchers, args, [arg | returned]) 935 | end 936 | defp bind_matcher_to_args(nil, [binding_ast | matchers], [arg_ast | args], returned) do 937 | arg = {:=, [], [binding_ast, arg_ast]} 938 | bind_matcher_to_args(nil, matchers, args, [arg | returned]) 939 | end 940 | defp bind_matcher_to_args(as, [{:when, _when_meta, [binding_ast, _when_call]}], args, []) do 941 | Enum.map(args, fn 942 | {^as, meta, scope} = arg when is_atom(scope) -> {:=, meta, [generify_matcher_binding(binding_ast), arg]} 943 | arg -> arg 944 | end) 945 | end 946 | defp bind_matcher_to_args(as, [binding_ast], args, []) do 947 | Enum.map(args, fn 948 | {^as, meta, scope} = arg when is_atom(scope) -> {:=, meta, [generify_matcher_binding(binding_ast), arg]} 949 | arg -> arg 950 | end) 951 | end 952 | 953 | defp generify_matcher_binding(binding_ast), do: binding_ast # TODO to allow multiple binding locations? 954 | 955 | defp replace_head_args_with(ast_head, head_args) 956 | defp replace_head_args_with({:def, meta, [{:when, when_meta, [{name, name_meta, _args}, guards]} | rest]}, head_args) do 957 | {:def, meta, [{:when, when_meta, [{name, name_meta, head_args}, guards]} | rest]} 958 | end 959 | defp replace_head_args_with({:def, meta, [{name, name_meta, _args} | rest]}, head_args) do 960 | {:def, meta, [{name, name_meta, head_args} | rest]} 961 | end 962 | 963 | defp get_guards_from_matchers(matchers, returned \\ []) 964 | defp get_guards_from_matchers([], []), do: true 965 | defp get_guards_from_matchers([], returned), do: Enum.reduce(:lists.reverse(returned), fn(ast, acc) -> {:and, [], [ast, acc]} end) 966 | defp get_guards_from_matchers([{:when, _when_meta, [_bindings, guard]} | matchers], returned) do 967 | get_guards_from_matchers(matchers, [guard | returned]) 968 | end 969 | defp get_guards_from_matchers([_when_ast | matchers], returned) do 970 | get_guards_from_matchers(matchers, returned) 971 | end 972 | 973 | defp add_guard_to_head(ast_head, guard) 974 | defp add_guard_to_head(ast_head, true), do: ast_head 975 | defp add_guard_to_head({:def, meta, [{:when, when_meta, [head, old_guard]} | rest]}, guard) do 976 | {:def, meta, [ 977 | {:when, when_meta, [head, {:and, [], [old_guard, guard]}]} 978 | | rest 979 | ]} 980 | end 981 | defp add_guard_to_head({:def, meta, [head | rest]}, guard) do 982 | {:def, meta, [ 983 | {:when, [], [head, guard]} 984 | | rest 985 | ]} 986 | end 987 | 988 | defp remove_guards(ast_head) 989 | defp remove_guards({:def, meta, [{:when, _when_meta, [head, _guard]} | body]}) do 990 | remove_guards({:def, meta, [head | body]}) 991 | end 992 | defp remove_guards(ast), do: ast 993 | 994 | defp build_body_call_with_args(module, name, args) do 995 | quote do 996 | unquote(module).unquote(name)(unquote_splicing(args)) 997 | end 998 | end 999 | 1000 | defp append_body_to_head(ast_head, body) 1001 | defp append_body_to_head({:def, meta, args}, body) do 1002 | {:def, meta, args++[[do: body]]} 1003 | end 1004 | 1005 | 1006 | defp generate_alias_usage(ast, env) do 1007 | Macro.prewalk(ast, [], fn 1008 | {:__aliases__, _ctx, [name | _]} = ast, acc -> 1009 | if env.aliases[Module.concat([name])] do 1010 | {ast, [quote(do: _ = unquote(ast)) | acc]} 1011 | else 1012 | {ast, acc} 1013 | end 1014 | ast, acc -> 1015 | {ast, acc} 1016 | end) 1017 | |> elem(1) 1018 | end 1019 | 1020 | 1021 | end 1022 | 1023 | 1024 | if Mix.env() === :test do 1025 | # MyDecimal for Numbers 1026 | defimpl Numbers.Protocols.Addition, for: Tuple do 1027 | def add({MyDecimal, _s0, :sNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 1028 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :sNaN, _e1}), do: throw :error 1029 | def add({MyDecimal, _s0, :qNaN, _e0} = d0, {MyDecimal, _s1, _c1, _e1}), do: d0 1030 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :qNaN, _e1} = d1), do: d1 1031 | def add({MyDecimal, s0, :inf, e0} = d0, {MyDecimal, s0, :inf, e1} = d1), do: if(e0 > e1, do: d0, else: d1) 1032 | def add({MyDecimal, _s0, :inf, _e0}, {MyDecimal, _s1, :inf, _e1}), do: throw :error 1033 | def add({MyDecimal, _s0, :inf, _e0} = d0, {MyDecimal, _s1, _c1, _e1}), do: d0 1034 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :inf, _e1} = d1), do: d1 1035 | def add({MyDecimal, s0, c0, e0}, {MyDecimal, s1, c1, e1}) do 1036 | {c0, c1} = 1037 | cond do 1038 | e0 === e1 -> {c0, c1} 1039 | e0 > e1 -> {c0 * pow10(e0 - e1), c1} 1040 | true -> {c0, c1 * pow10(e1 - e0)} 1041 | end 1042 | c = s0 * c0 + s1 * c1 1043 | e = Kernel.min(e0, e1) 1044 | s = 1045 | cond do 1046 | c > 0 -> 1 1047 | c < 0 -> -1 1048 | s0 == -1 and s1 == -1 -> -1 1049 | # s0 != s1 and get_context().rounding == :floor -> -1 1050 | true -> 1 1051 | end 1052 | {s, Kernel.abs(c), e} 1053 | end 1054 | def mult({MyDecimal, s0, c0, e0}, {MyDecimal, s1, c1, e1}) do 1055 | s = s0 * s1 1056 | {s, c0 * c1, e0 + e1} 1057 | end 1058 | def add_id(_num), do: {MyDecimal, 1, 0, 0} 1059 | 1060 | _pow10_max = Enum.reduce 0..104, 1, fn int, acc -> 1061 | def pow10(unquote(int)), do: unquote(acc) 1062 | def base10?(unquote(acc)), do: true 1063 | acc * 10 1064 | end 1065 | def pow10(num) when num > 104, do: pow10(104) * pow10(num - 104) 1066 | end 1067 | 1068 | defimpl Numbers.Protocols.Multiplication, for: Tuple do 1069 | def mult({MyDecimal, _s0, :sNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 1070 | def mult({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :sNaN, _e1}), do: throw :error 1071 | def mult({MyDecimal, _s0, :qNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 1072 | def mult({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :qNaN, _e1}), do: throw :error 1073 | def mult({MyDecimal, _s0, 0, _e0}, {MyDecimal, _s1, :inf, _e1}), do: throw :error 1074 | def mult({MyDecimal, _s0, :inf, _e0}, {MyDecimal, _s1, 0, _e1}), do: throw :error 1075 | def mult({MyDecimal, s0, :inf, e0}, {MyDecimal, s1, _, e1}) do 1076 | s = s0 * s1 1077 | {s, :inf, e0+e1} 1078 | end 1079 | def mult({MyDecimal, s0, _, e0}, {MyDecimal, s1, :inf, e1}) do 1080 | s = s0 * s1 1081 | {s, :inf, e0+e1} 1082 | end 1083 | def mult({MyDecimal, s0, c0, e0}, {MyDecimal, s1, c1, e1}) do 1084 | s = s0 * s1 1085 | {s, c0 * c1, e0 + e1} 1086 | end 1087 | def mult_id(_num), do: {MyDecimal, 1, 1, 0} 1088 | end 1089 | end 1090 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ProtocolEx.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :protocol_ex, 7 | version: "0.5.0", 8 | elixir: "~> 1.15", 9 | description: description(), 10 | package: package(), 11 | docs: [ 12 | extras: ["README.md"], 13 | main: "readme", 14 | #markdown_processor: ExDocMakeup, 15 | #markdown_processor_options: [ 16 | # lexer_options: %{ 17 | # "elixir" => [ 18 | # extra_declarations: [ 19 | # "defimplEx", "defimpl_ex", 20 | # "defprotocolEx", "defprotocol_ex"], 21 | # extra_def_like: ["deftest"]] 22 | # } 23 | #] 24 | ], 25 | build_embedded: Mix.env == :prod, 26 | start_permanent: Mix.env == :prod, 27 | deps: deps(), 28 | ] 29 | end 30 | 31 | def description do 32 | """ 33 | Extended Protocol library using Matchers 34 | """ 35 | end 36 | 37 | def package do 38 | [ 39 | licenses: ["MIT"], 40 | name: :protocol_ex, 41 | maintainers: ["OvermindDL1"], 42 | links: %{"Github" => "https://github.com/OvermindDL1/protocol_ex"} 43 | ] 44 | end 45 | 46 | def application do 47 | # Specify extra applications you'll use from Erlang/Elixir 48 | [ 49 | extra_applications: [ 50 | # :logger, 51 | ] ++ if(Mix.env() in [:test], do: [:stream_data], else: []) 52 | ] 53 | end 54 | 55 | defp deps do 56 | [ 57 | # Optional dependencies 58 | {:stream_data, "~> 0.5.0", optional: true, only: [:dev, :test]}, 59 | # Documentation 60 | {:ex_doc, ">= 0.19.0-rc", only: [:dev]}, 61 | #{:ex_doc_makeup, ">= 0.1.0", only: [:dev]}, 62 | # Testing only 63 | {:cortex, "~> 0.5.0", only: [:test]}, 64 | {:benchee, "~> 0.14.0", only: [:test]}, 65 | {:numbers, "~> 5.1", only: [:test]}, 66 | {:decimal, "~> 1.3", only: [:test]} 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "0.14.0", "f771f587c48b4824b497e2a3e374f75e93ef01fc329873b089a3f5dd961b80b8", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "d4ccc9e6b5673c62bb95ca53fb7a7ee7f4f4d94b9f0e8f88b1ef08bfd7d2213d"}, 3 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 4 | "cortex": {:hex, :cortex, "0.5.0", "7b12ddcfa9fd61521dd3b841ef8a4758ccab895d6a5d4ed181a5a8ad4b4592a4", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "3352cd4c67d7cc3cf0c2858bea48c0112c255cdfb665e6d9393a47328c36aa42"}, 5 | "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, 6 | "deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm", "e3bf435a54ed27b0ba3a01eb117ae017988804e136edcbe8a6a14c310daa966e"}, 7 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 9 | "ex_const": {:hex, :ex_const, "0.1.0", "7b04d0b65f00cfb8afc0b778bd58dec4804612a54f57122524ee097e9f2c5443", [:mix], []}, 10 | "ex_doc": {:hex, :ex_doc, "0.33.0", "690562b153153c7e4d455dc21dab86e445f66ceba718defe64b0ef6f0bd83ba0", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "3f69adc28274cb51be37d09b03e4565232862a4b10288a3894587b0131412124"}, 11 | "ex_spirit": {:hex, :ex_spirit, "0.3.4", "cf21c0fa0349445da40091df1fb8141d0e802f83b83659132713012bbe7fb398", [:mix], [], "hexpm"}, 12 | "exconstructor": {:hex, :exconstructor, "1.1.0", "272623a7b203cb2901c20cbb92c5c3ab103cc0087ff7c881979e046043346752", [:mix], []}, 13 | "exfswatch": {:hex, :exfswatch, "0.4.2", "d88a63b5c2f8f040230d22010588ff73286fd1aef32564115afa3051eaa4391d", [:mix], []}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "html_entities": {:hex, :html_entities, "0.3.0", "2f278ffc69c3f0cbd5996628fef37cf922fa715f3e04b9f38924c8adced3f25a", [:mix], []}, 16 | "makedown": {:hex, :makedown, "0.2.0", "f47c34e6cc6ddb81a6306c63a580e49fb2c9eea0eec841e579891f1899477280", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}, {:makeup, "~> 0.2.0", [hex: :makeup, optional: false]}]}, 17 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 18 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 19 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 21 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 22 | "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, 23 | } 24 | -------------------------------------------------------------------------------- /test/bench/numbers_test.exs: -------------------------------------------------------------------------------- 1 | import ProtocolEx 2 | 3 | defprotocolEx Basic do 4 | def add(a, b) 5 | def mult(a, b) 6 | end 7 | 8 | defprotocolEx Coerce do 9 | def coerce({l, r}) 10 | def coerce(l, r), do: coerce({l, r}) 11 | end 12 | 13 | defimplEx Decimal, %Decimal{}, for: Basic, inline: [add: 2, mult: 2] do 14 | def add(%Decimal{}=a, b), do: Decimal.add(a, b) 15 | def mult(%Decimal{}=a, b), do: Decimal.mult(a, b) 16 | end 17 | 18 | # Do not inline cavalier, it changes the function scope to the protocol, 19 | # so local calls will not work unless they are to the protocol, nor alias or anything not in the function scope. 20 | defimplEx Integer, i when is_integer(i), for: Basic, inline: [add: 2, mult: 2] do 21 | def add(a, b) when is_integer(a), do: a + b 22 | def mult(a, b) when is_integer(b), do: a * b 23 | end 24 | 25 | defimplEx Float, f when is_float(f), for: Basic, inline: [add: 2, mult: 2] do 26 | def add(a, b) when is_float(a), do: a + b 27 | def mult(a, b) when is_float(a), do: a * b 28 | end 29 | 30 | defimplEx MyDecimal, {MyDecimal, s, c, e}, for: Basic, inline: [add: 2, mult: 2] do 31 | def add({MyDecimal, _s0, :sNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 32 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :sNaN, _e1}), do: throw :error 33 | def add({MyDecimal, _s0, :qNaN, _e0} = d0, {MyDecimal, _s1, _c1, _e1}), do: d0 34 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :qNaN, _e1} = d1), do: d1 35 | def add({MyDecimal, s0, :inf, e0} = d0, {MyDecimal, s0, :inf, e1} = d1), do: if(e0 > e1, do: d0, else: d1) 36 | def add({MyDecimal, _s0, :inf, _e0}, {MyDecimal, _s1, :inf, _e1}), do: throw :error 37 | def add({MyDecimal, _s0, :inf, _e0} = d0, {MyDecimal, _s1, _c1, _e1}), do: d0 38 | def add({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :inf, _e1} = d1), do: d1 39 | def add({MyDecimal, s0, c0, e0}, {MyDecimal, s1, c1, e1}) do 40 | {c0, c1} = 41 | cond do 42 | e0 === e1 -> {c0, c1} 43 | e0 > e1 -> {c0 * Basic.MyDecimal.pow10(e0 - e1), c1} 44 | true -> {c0, c1 * Basic.MyDecimal.pow10(e1 - e0)} 45 | end 46 | c = s0 * c0 + s1 * c1 47 | e = Kernel.min(e0, e1) 48 | s = 49 | cond do 50 | c > 0 -> 1 51 | c < 0 -> -1 52 | s0 == -1 and s1 == -1 -> -1 53 | # s0 != s1 and get_context().rounding == :floor -> -1 54 | true -> 1 55 | end 56 | {s, Kernel.abs(c), e} 57 | end 58 | 59 | def mult({MyDecimal, _s0, :sNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 60 | def mult({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :sNaN, _e1}), do: throw :error 61 | def mult({MyDecimal, _s0, :qNaN, _e0}, {MyDecimal, _s1, _c1, _e1}), do: throw :error 62 | def mult({MyDecimal, _s0, _c0, _e0}, {MyDecimal, _s1, :qNaN, _e1}), do: throw :error 63 | def mult({MyDecimal, _s0, 0, _e0}, {MyDecimal, _s1, :inf, _e1}), do: throw :error 64 | def mult({MyDecimal, _s0, :inf, _e0}, {MyDecimal, _s1, 0, _e1}), do: throw :error 65 | def mult({MyDecimal, s0, :inf, e0}, {MyDecimal, s1, _, e1}) do 66 | s = s0 * s1 67 | {s, :inf, e0+e1} 68 | end 69 | def mult({MyDecimal, s0, _, e0}, {MyDecimal, s1, :inf, e1}) do 70 | s = s0 * s1 71 | {s, :inf, e0+e1} 72 | end 73 | def mult({MyDecimal, s0, c0, e0}, {MyDecimal, s1, c1, e1}) do 74 | s = s0 * s1 75 | {s, c0 * c1, e0 + e1} 76 | end 77 | 78 | _pow10_max = Enum.reduce 0..104, 1, fn int, acc -> 79 | def pow10(unquote(int)), do: unquote(acc) 80 | def base10?(unquote(acc)), do: true 81 | acc * 10 82 | end 83 | def pow10(num) when num > 104, do: pow10(104) * pow10(num - 104) 84 | end 85 | 86 | # Coerce 87 | defimplEx IntegerFloat, {l, r} when (is_integer(l) and is_float(r)) or (is_integer(r) and is_float(l)), for: Coerce, inline: [coerce: 1] do 88 | def coerce({l, r}) when (is_integer(l) and is_float(r)) or (is_integer(r) and is_float(l)), do: {0.0+l, 0.0+r} 89 | end 90 | defimplEx Integer, {l, r} when is_integer(l) and is_integer(r), for: Coerce, inline: [coerce: 1] do 91 | def coerce({l, r}) when is_integer(l) and is_integer(r), do: {l, r} 92 | end 93 | defimplEx Float, {l, r} when is_float(l) and is_float(r), for: Coerce, inline: [coerce: 1] do 94 | def coerce({l, r}) when is_float(l) and is_float(r), do: {l, r} 95 | end 96 | defimplEx Decimal,{%Decimal{}, %Decimal{}}, for: Coerce, inline: [coerce: 1] do 97 | def coerce({%Decimal{}, %Decimal{}} = result), do: result 98 | end 99 | defimplEx MyDecimal,{{MyDecimal, _, _, _}, {MyDecimal, _, _, _}}, for: Coerce, inline: [coerce: 1] do 100 | def coerce({{MyDecimal, _, _, _}, {MyDecimal, _, _, _}} = result), do: result 101 | end 102 | 103 | 104 | defmodule NumbersResolved do 105 | resolveProtocolEx(Basic, [ 106 | Integer, 107 | Float, 108 | Decimal, 109 | MyDecimal, 110 | ]) 111 | 112 | resolveProtocolEx(Coerce, [ 113 | Integer, 114 | Float, 115 | Decimal, 116 | MyDecimal, 117 | IntegerFloat, 118 | ]) 119 | end 120 | 121 | 122 | 123 | defmodule ProtocolEx.Bench.NumbersTest do 124 | use ExUnit.Case, async: false 125 | @moduletag :bench 126 | @moduletag timeout: 300000 127 | 128 | test "Numbers - Bench" do 129 | inputs = %{ 130 | "Integers" => {7, 11}, 131 | "Floats" => {6.28, 4.24}, 132 | "Decimal" => {Decimal.div(Decimal.new(8), Decimal.new(3)), Decimal.new(2)}, 133 | "MyDecimal" => {{MyDecimal, 1, 2666666666666666666666666667, -27}, {MyDecimal, 1, 8, 0}} 134 | } 135 | 136 | bench = %{ 137 | "MyNumbers" => fn {l, r} -> {l, r} = Coerce.coerce(l, r); Basic.add(l, r) end, 138 | "MyNumbers - sans coerce" => fn {l, r} ->Basic.add(l, r) end, 139 | "Numbers" => fn {l, r} -> Numbers.add(l, r) end, 140 | "Numbers - sans coerce" => fn {l, r} -> Numbers.Protocols.Addition.add(l, r) end, 141 | "Numbers - my coerce" => fn {l, r} -> {l, r} = Coerce.coerce(l, r); Numbers.Protocols.Addition.add(l, r) end, 142 | } 143 | 144 | Benchee.run(bench, inputs: inputs, time: 3, warmup: 3, print: [fast_warning: false]) 145 | end 146 | 147 | end 148 | -------------------------------------------------------------------------------- /test/protocol_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ProtocolExTest do 2 | use ExUnit.Case, async: true 3 | doctest ProtocolEx 4 | 5 | test "the truth" do 6 | assert 0 === Blah.Integer.empty() 7 | 8 | assert 0 === Blah.empty(42) 9 | assert {Vwoop, 0} === Blah.empty({Vwoop, 42}) 10 | assert %MyStruct{a: 0} === Blah.empty(%MyStruct{}) 11 | 12 | assert 43 === Blah.succ(42) 13 | assert {Vwoop, 43} === Blah.succ({Vwoop, 42}) 14 | assert %MyStruct{a: 43} === Blah.succ(%MyStruct{a: 42}) 15 | 16 | assert 43 === Blah.add(42, 1) 17 | assert {Vwoop, 43} === Blah.add({Vwoop, 42}, 1) 18 | assert %MyStruct{a: 43} === Blah.add(%MyStruct{a: 42}, 1) 19 | end 20 | 21 | #test "Failure" do 22 | # assert 42 == Blah.add("42", "16") 23 | #end 24 | 25 | test "Aliasing" do 26 | alias Mod1.Mod11.Mod111 27 | assert %Mod111{a: 0} = ModProto.blah(%Mod111{a: 1}) 28 | end 29 | 30 | test "Defaults" do 31 | assert 2 = Defaults.succ(1) 32 | assert 3 = Defaults.succ(1, 2) 33 | end 34 | 35 | 36 | use ExUnitProperties 37 | 38 | property "Integers in the Blah protocol" do 39 | check all(i <- integer(), j <- integer()) do 40 | assert 0 === Blah.empty(i) 41 | assert (i + 1) === Blah.succ(i) 42 | assert (i + j) === Blah.add(i, j) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [bench: true]) 2 | ExUnit.start() 3 | 4 | 5 | defmodule MyStruct do 6 | defstruct a: 42 7 | end 8 | 9 | import ProtocolEx 10 | 11 | defmodule EmbTesting do 12 | defprotocol_ex Emb do 13 | def add(a) 14 | end 15 | 16 | defimpl_ex Atom, a when is_atom(a), for: Emb do 17 | def add(a), do: a 18 | end 19 | defimpl_ex Binary, b when is_binary(b), for: EmbTesting.Emb do 20 | def add(b), do: b 21 | end 22 | end 23 | 24 | ## This can't be in the same file as the module definition as the module 25 | ## is not compiled by the time this is ready. 26 | #defimpl_ex Integer, i when is_integer(i), for: EmbTesting.Emb do 27 | # def add(a), do: a+1 28 | #end 29 | 30 | defmodule MoreEmbTesting do 31 | defimpl_ex Float, f when is_float(f), for: EmbTesting.Emb do 32 | def add(a), do: a+1.0 33 | end 34 | end 35 | 36 | defprotocol_ex Blah do 37 | def empty() 38 | def succ(a) 39 | def add(a, b) 40 | def map(a, f) when is_function(f, 1) 41 | 42 | def a_fallback(a), do: nil # inspect(a) 43 | end 44 | 45 | defimpl_ex Integer, i when is_integer(i), for: Blah do 46 | @priority 1 47 | def empty(), do: 0 48 | defmacro succ(ivar), do: quote(do: unquote(ivar)+1) 49 | def add(i, b), do: i+b 50 | def map(i, f), do: f.(i) 51 | 52 | def a_fallback(i), do: "Integer: #{i}" 53 | end 54 | 55 | defimplEx TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do 56 | def empty(), do: {Vwoop, 0} 57 | def succ({Vwoop, i}), do: {Vwoop, i+1} 58 | def add({Vwoop, i}, b), do: {Vwoop, i+b} 59 | def map({Vwoop, i}, f), do: {Vwoop, f.(i)} 60 | end 61 | 62 | defimplEx MineOlStruct, %MyStruct{}, for: Blah do 63 | def empty(), do: %MyStruct{a: extra_func1(0)} 64 | def succ(s), do: %{s | a: s.a+1} 65 | def add(s, b), do: %{s | a: s.a+b} 66 | def map(s, f), do: %{s | a: f.(s.a)} 67 | def extra_func0(), do: priv_func0() 68 | def extra_func1(a), do: priv_func1(a) 69 | def extra_func2(a, b), do: priv_func2(a, b) 70 | defp priv_func0, do: :undefined 71 | defp priv_func1(a), do: a 72 | defp priv_func2(a, b), do: a+b 73 | end 74 | 75 | defmodule Resolver do 76 | ProtocolEx.resolveProtocolEx(Blah, [ 77 | Integer, 78 | TaggedTuple.Vwoop, 79 | MineOlStruct, 80 | ]) 81 | end 82 | 83 | defmodule Mod1 do 84 | defmodule Mod11 do 85 | defmodule Mod111 do 86 | defstruct a: 42 87 | end 88 | end 89 | end 90 | 91 | defprotocol_ex ModProto do 92 | @moduledoc "Test moduledoc" 93 | @doc "Test blah docs" 94 | def blah(a) 95 | @doc "Test bloop docs" 96 | def bloop(a) 97 | end 98 | 99 | defimpl_ex Greater, %Mod1.Mod11.Mod111{a: a} when a>=0, for: ModProto do 100 | @doc "and here!" 101 | def blah(%Mod1.Mod11.Mod111{a: a}), do: %Mod1.Mod11.Mod111{a: a - 1} 102 | def bloop(v), do: v 103 | end 104 | alias Mod1.Mod11 105 | defimpl_ex Zero, %Mod11.Mod111{a: 0}, for: ModProto do 106 | def blah(%Mod11.Mod111{a: a}), do: %Mod11.Mod111{a: a} 107 | alias Mod11.Mod111 108 | def bloop(%Mod111{a: a}), do: %Mod111{a: a} 109 | end 110 | alias Mod11.Mod111 111 | defimpl_ex Lesser, %Mod111{a: a} when a<0, for: ModProto do 112 | def blah(%Mod111{a: a}), do: %Mod111{a: a + 1} 113 | def bloop(v), do: v 114 | end 115 | defmodule ResolverMod do 116 | ProtocolEx.resolveProtocolEx(ModProto, [Zero, Greater, Lesser]) 117 | end 118 | 119 | 120 | defprotocolEx Expro.Proto do 121 | def foo(p) 122 | end 123 | 124 | alias Expro.Proto 125 | 126 | defimplEx Foo, true, for: Proto do 127 | def foo(_p), do: true 128 | end 129 | 130 | defmodule ResolverExpro do 131 | ProtocolEx.resolveProtocolEx(Expro.Proto, [Foo]) 132 | end 133 | 134 | 135 | defprotocol_ex Defaults do 136 | def succ(a, b \\ 1) 137 | end 138 | 139 | defimpl_ex Integer, i when is_integer(i), for: Defaults do 140 | def succ(a, b), do: a + b 141 | end 142 | 143 | defmodule DefaultResolver do 144 | ProtocolEx.resolveProtocolEx(Defaults, [Integer]) 145 | end 146 | 147 | 148 | # # TODO: Actually I do have to wrap it up now, will not work until they actually exist in `.ex` files now... 149 | # # Only wrapping everything up in modules to prevent having to make more `.ex` files 150 | # defmodule Testering do 151 | # 152 | # defprotocol_ex Blah do 153 | # def empty() 154 | # def succ(a) 155 | # def add(a, b) 156 | # def map(a, f) when is_function(f, 1) 157 | # 158 | # def a_fallback(a), do: inspect(a) 159 | # end 160 | # 161 | # # Extending and properties 162 | # defprotocolEx Functor, as: v do 163 | # def map(v, f) 164 | # 165 | # deftest identity do 166 | # StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v -> 167 | # if v === map(v, &(&1)) do 168 | # {:ok, v} 169 | # else 170 | # {:error, v} 171 | # end 172 | # end) 173 | # end 174 | # 175 | # deftest composition do 176 | # f = fn x -> x end 177 | # g = fn x -> x end 178 | # StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v -> 179 | # if map(v, fn x -> f.(g.(x)) end) === map(map(v, g), f) do 180 | # {:ok, v} 181 | # else 182 | # {:error, v} 183 | # end 184 | # end) 185 | # end 186 | # end 187 | # 188 | # end 189 | # 190 | # defmodule Testering1 do 191 | # alias Testering.Blah 192 | # alias Testering.Functor 193 | # 194 | # defimpl_ex Integer, i when is_integer(i), for: Blah do 195 | # @priority 1 196 | # def empty(), do: 0 197 | # defmacro succ(ivar), do: quote(do: unquote(ivar)+1) 198 | # def add(i, b), do: i+b 199 | # def map(i, f), do: f.(i) 200 | # 201 | # def a_fallback(i), do: "Integer: #{i}" 202 | # end 203 | # 204 | # defimplEx TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do 205 | # def empty(), do: {Vwoop, 0} 206 | # def succ({Vwoop, i}), do: {Vwoop, i+1} 207 | # def add({Vwoop, i}, b), do: {Vwoop, i+b} 208 | # def map({Vwoop, i}, f), do: {Vwoop, f.(i)} 209 | # end 210 | # 211 | # defimplEx MineOlStruct, %MyStruct{}, for: Blah do 212 | # def empty(), do: %MyStruct{a: 0} 213 | # def succ(s), do: %{s | a: s.a+1} 214 | # def add(s, b), do: %{s | a: s.a+b} 215 | # def map(s, f), do: %{s | a: f.(s.a)} 216 | # end 217 | # 218 | # # Functor test 219 | # 220 | # defimplEx Integer, i when is_integer(i), for: Functor, inline: [map: 2] do 221 | # def prop_generator(), do: StreamData.integer() 222 | # def map(i, f) when is_integer(i), do: f.(i) 223 | # end 224 | # 225 | # defimplEx List, l when is_list(l), for: Functor do 226 | # def prop_generator(), do: StreamData.list_of(StreamData.integer()) 227 | # def map([], _f), do: [] 228 | # def map([h | t], f), do: [f.(h) | map(t, f)] 229 | # end 230 | # end 231 | # 232 | # defmodule TesteringResolved do # This thing could easily become a compiler plugin instead of an explicit call 233 | # alias Testering.Blah 234 | # alias Testering.Functor 235 | # 236 | # ProtocolEx.resolveProtocolEx(Blah, [ 237 | # Integer, 238 | # TaggedTuple.Vwoop, 239 | # MineOlStruct, 240 | # ]) 241 | # 242 | # ProtocolEx.resolve_protocol_ex(Functor, [ 243 | # Integer, 244 | # List, 245 | # ]) 246 | # 247 | # # Now supporting auto-detection of anything already compiled! 248 | # # (So when inline at compile-time like this then require first to make sure they are already compiled) 249 | # # require Blah.Integer 250 | # # require Blah.TaggedTuple.Vwoop 251 | # # require Blah.MineOlStruct 252 | # # ProtocolEx.resolveProtocolEx(Blah) # Without a list it auto-detects based on what is already compiled 253 | # 254 | # 0 = Blah.Integer.empty() 255 | # {Vwoop, 0} = Blah.TaggedTuple.Vwoop.empty() 256 | # %MyStruct{a: 0} = Blah.MineOlStruct.empty() 257 | # 258 | # 0 = Blah.empty(42) 259 | # {Vwoop, 0} = Blah.empty({Vwoop, 42}) 260 | # %MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42}) 261 | # 262 | # 43 = Blah.succ(42) 263 | # {Vwoop, 43} = Blah.succ({Vwoop, 42}) 264 | # %MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42}) 265 | # 266 | # 47 = Blah.add(42, 5) 267 | # {Vwoop, 47} = Blah.add({Vwoop, 42}, 5) 268 | # %MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5) 269 | # 270 | # "Integer: 42" = Blah.a_fallback(42) 271 | # "{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42}) 272 | # "%MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42}) 273 | # 274 | # 43 = Blah.map(42, &(&1+1)) 275 | # {Vwoop, 43} = Blah.map({Vwoop, 42}, &(&1+1)) 276 | # %MyStruct{a: 43} = Blah.map(%MyStruct{a: 42}, &(&1+1)) 277 | # end 278 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/README.md: -------------------------------------------------------------------------------- 1 | # ConsolidateTest 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `consolidate_test` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:consolidate_test, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/consolidate_test](https://hexdocs.pm/consolidate_test). 21 | 22 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :consolidate_test, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:consolidate_test, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/lib/blah.ex: -------------------------------------------------------------------------------- 1 | import ProtocolEx 2 | 3 | defprotocolEx Blah do 4 | @moduledoc "Test moduledoc" 5 | 6 | @doc "Test empty doc" 7 | #@doc since: "1.0.0" 8 | def empty(a) 9 | def succ(a) 10 | def add(a, b) 11 | def map(a, f) when is_function(f, 1) 12 | 13 | def a_fallback(a), do: inspect(a) 14 | 15 | deftest identity do 16 | id = fn x -> x end 17 | StreamData.check_all(StreamData.integer(), [initial_seed: :os.timestamp()], fn a -> 18 | v = empty(nil) 19 | if map(v, id) === v do # and a<10 do 20 | {:ok, a} 21 | else 22 | {:error, a} 23 | end 24 | end) 25 | end 26 | end 27 | 28 | defmodule Bloop do 29 | defprotocolEx Bloop do 30 | def get(thing) 31 | def get_with_fallback(thing), do: {:fallback, thing} 32 | end 33 | end 34 | 35 | #defmodule Bloop.Bloop do 36 | # def blah do 37 | # end 38 | #end 39 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/lib/blah_impls.ex: -------------------------------------------------------------------------------- 1 | import ProtocolEx 2 | 3 | defimplEx Integer, i when is_integer(i), for: Blah do 4 | @priority 1 5 | def empty(_), do: 0 6 | def succ(i), do: i+1 7 | def add(i, b), do: i+b 8 | def map(i, f), do: f.(i) 9 | 10 | def a_fallback(i), do: "Integer: #{i}" 11 | end 12 | 13 | defmodule SubModule.MyStruct do 14 | defstruct a: 42 15 | end 16 | 17 | defimplEx TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do 18 | def empty(_), do: {Vwoop, 0} 19 | def succ({Vwoop, i}), do: {Vwoop, i+1} 20 | def add({Vwoop, i}, b), do: {Vwoop, i+b} 21 | def map({Vwoop, i}, f), do: {Vwoop, f.(i)} 22 | end 23 | 24 | alias SubModule.MyStruct 25 | defimplEx MineOlStruct, %MyStruct{}, for: Blah do 26 | def empty(_), do: %SubModule.MyStruct{a: 0} 27 | def succ(s), do: %{s | a: s.a+1} 28 | def add(s, b), do: %{s | a: s.a+b} 29 | def map(s, f), do: %{s | a: f.(s.a)} 30 | end 31 | 32 | defimplEx Integer, i when is_integer(i), for: Bloop.Bloop do 33 | def get(i), do: {:integer, i} 34 | def get_with_fallback(i), do: {:integer, i} 35 | end 36 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/lib/consolidate_test.ex: -------------------------------------------------------------------------------- 1 | 2 | # Auto-consolidation by the compiler now if hooked in 3 | 4 | # require :"Elixir.Blah.$ProtocolEx_description$" 5 | # require Blah.Integer 6 | # require Blah.TaggedTuple.Vwoop 7 | # require Blah.MineOlStruct 8 | # ProtocolEx.consolidate(Blah) 9 | # ProtocolEx.consolidate_all() 10 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ConsolidateTest.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :consolidate_test, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env == :prod, 10 | compilers: Mix.compilers ++ [:protocol_ex], 11 | deps: deps(), 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:protocol_ex, path: "../.."}, 26 | {:stream_data, "~> 0.4.2"}, 27 | {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false}, 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, 3 | "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm"}, 4 | } 5 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/test/consolidate_test_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ConsolidateTestTest do 2 | use ExUnit.Case 3 | 4 | alias SubModule.MyStruct 5 | 6 | test "Blah" do 7 | assert 0 = Blah.empty(42) 8 | assert {Vwoop, 0} = Blah.empty({Vwoop, 42}) 9 | assert %MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42}) 10 | 11 | assert 43 = Blah.succ(42) 12 | assert {Vwoop, 43} = Blah.succ({Vwoop, 42}) 13 | assert %MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42}) 14 | 15 | assert 47 = Blah.add(42, 5) 16 | assert {Vwoop, 47} = Blah.add({Vwoop, 42}, 5) 17 | assert %MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5) 18 | 19 | assert "Integer: 42" = Blah.a_fallback(42) 20 | assert "{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42}) 21 | assert "%SubModule.MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42}) 22 | 23 | assert 43 = Blah.map(42, &(&1+1)) 24 | assert {Vwoop, 43} = Blah.map({Vwoop, 42}, &(&1+1)) 25 | assert %MyStruct{a: 43} = Blah.map(%MyStruct{a: 42}, &(&1+1)) 26 | 27 | alias Bloop.Bloop 28 | assert {:integer, 42} = Bloop.get(42) 29 | assert {:integer, 42} = Bloop.get_with_fallback(42) 30 | assert {:fallback, 6.28} = Bloop.get_with_fallback(6.28) 31 | 32 | #assert_raise ProtocolEx.UnimplementedProtocolEx, fn -> 33 | assert_raise FunctionClauseError, fn -> 34 | Bloop.get(6.28) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /tests_extra/consolidate_test/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "config/*.exs"], 4 | subdirectories: ["apps/*"] 5 | ] 6 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/README.md: -------------------------------------------------------------------------------- 1 | # UmbrellaTest 2 | 3 | **TODO: Add description** 4 | 5 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | app1-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/README.md: -------------------------------------------------------------------------------- 1 | # App1 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `app1` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:app1, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/lib/app1.ex: -------------------------------------------------------------------------------- 1 | defmodule App1 do 2 | @moduledoc """ 3 | Documentation for `App1`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> App1.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/lib/blah_impls.ex: -------------------------------------------------------------------------------- 1 | import ProtocolEx 2 | 3 | defimplEx Integer, i when is_integer(i), for: Blah do 4 | @priority 1 5 | def empty(_), do: 0 6 | def succ(i), do: i+1 7 | def add(i, b), do: i+b 8 | def map(i, f), do: f.(i) 9 | 10 | def a_fallback(i), do: "Integer: #{i}" 11 | end 12 | 13 | defmodule SubModule.MyStruct do 14 | defstruct a: 42 15 | end 16 | 17 | defimplEx TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do 18 | def empty(_), do: {Vwoop, 0} 19 | def succ({Vwoop, i}), do: {Vwoop, i+1} 20 | def add({Vwoop, i}, b), do: {Vwoop, i+b} 21 | def map({Vwoop, i}, f), do: {Vwoop, f.(i)} 22 | end 23 | 24 | alias SubModule.MyStruct 25 | defimplEx MineOlStruct, %MyStruct{}, for: Blah do 26 | def empty(_), do: %SubModule.MyStruct{a: 0} 27 | def succ(s), do: %{s | a: s.a+1} 28 | def add(s, b), do: %{s | a: s.a+b} 29 | def map(s, f), do: %{s | a: f.(s.a)} 30 | end 31 | 32 | defimplEx Integer, i when is_integer(i), for: Bloop.Bloop do 33 | def get(i), do: {:integer, i} 34 | def get_with_fallback(i), do: {:integer, i} 35 | end 36 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule App1.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :app1, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | compilers: Mix.compilers ++ [:protocol_ex], 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | {:lib1, in_umbrella: true}, 28 | {:lib2, in_umbrella: true}, 29 | {:protocol_ex, path: "../../../.."}, 30 | {:stream_data, "~> 0.4.3"}, 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "stream_data": {:hex, :stream_data, "0.4.3", "62aafd870caff0849a5057a7ec270fad0eb86889f4d433b937d996de99e3db25", [:mix], [], "hexpm", "7dafd5a801f0bc897f74fcd414651632b77ca367a7ae4568778191fc3bf3a19a"}, 3 | } 4 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/test/app1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule App1Test do 2 | use ExUnit.Case 3 | doctest App1 4 | 5 | alias SubModule.MyStruct 6 | 7 | test "Blah" do 8 | assert 0 = Blah.empty(42) 9 | assert {Vwoop, 0} = Blah.empty({Vwoop, 42}) 10 | assert %MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42}) 11 | 12 | assert 43 = Blah.succ(42) 13 | assert {Vwoop, 43} = Blah.succ({Vwoop, 42}) 14 | assert %MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42}) 15 | 16 | assert 47 = Blah.add(42, 5) 17 | assert {Vwoop, 47} = Blah.add({Vwoop, 42}, 5) 18 | assert %MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5) 19 | 20 | assert "Integer: 42" = Blah.a_fallback(42) 21 | assert "{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42}) 22 | assert "%SubModule.MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42}) 23 | 24 | assert 43 = Blah.map(42, &(&1+1)) 25 | assert {Vwoop, 43} = Blah.map({Vwoop, 42}, &(&1+1)) 26 | assert %MyStruct{a: 43} = Blah.map(%MyStruct{a: 42}, &(&1+1)) 27 | 28 | alias Bloop.Bloop 29 | assert {:integer, 42} = Bloop.get(42) 30 | assert {:integer, 42} = Bloop.get_with_fallback(42) 31 | assert {:float, 6.28} = Bloop.get(6.28) 32 | assert {:fallback, 6.28} = Bloop.get_with_fallback(6.28) 33 | 34 | #assert_raise ProtocolEx.UnimplementedProtocolEx, fn -> 35 | assert_raise FunctionClauseError, fn -> 36 | Bloop.get(6.28) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/app1/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | lib1-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/README.md: -------------------------------------------------------------------------------- 1 | # Lib1 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `lib1` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:lib1, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/lib/blah.ex: -------------------------------------------------------------------------------- 1 | import ProtocolEx 2 | 3 | defprotocolEx Blah do 4 | @moduledoc "Test moduledoc" 5 | 6 | @doc "Test empty doc" 7 | #@doc since: "1.0.0" 8 | def empty(a) 9 | def succ(a) 10 | def add(a, b) 11 | def map(a, f) when is_function(f, 1) 12 | 13 | def a_fallback(a), do: inspect(a) 14 | 15 | deftest identity do 16 | id = fn x -> x end 17 | StreamData.check_all(StreamData.integer(), [initial_seed: :os.timestamp()], fn a -> 18 | v = empty(nil) 19 | if map(v, id) === v do # and a<10 do 20 | {:ok, a} 21 | else 22 | {:error, a} 23 | end 24 | end) 25 | end 26 | end 27 | 28 | defmodule Bloop do 29 | defprotocolEx Bloop do 30 | def get(thing) 31 | def get_with_fallback(thing), do: {:fallback, thing} 32 | end 33 | end 34 | 35 | #defmodule Bloop.Bloop do 36 | # def blah do 37 | # end 38 | #end 39 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/lib/lib1.ex: -------------------------------------------------------------------------------- 1 | defmodule Lib1 do 2 | @moduledoc """ 3 | Documentation for `Lib1`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Lib1.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib1.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lib1, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | compilers: Mix.compilers ++ [:protocol_ex], 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | {:protocol_ex, path: "../../../.."}, 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/test/lib1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib1Test do 2 | use ExUnit.Case 3 | doctest Lib1 4 | 5 | test "greets the world" do 6 | assert Lib1.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib1/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | lib2-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/README.md: -------------------------------------------------------------------------------- 1 | # Lib2 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `lib2` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:lib2, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/lib/lib2.ex: -------------------------------------------------------------------------------- 1 | defmodule Lib2 do 2 | 3 | end 4 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib2.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lib2, 7 | version: "0.1.0", 8 | elixir: "~> 1.17-dev", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/test/lib2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib2Test do 2 | use ExUnit.Case 3 | doctest Lib2 4 | 5 | test "greets the world" do 6 | assert Lib2.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib2/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | lib3-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/README.md: -------------------------------------------------------------------------------- 1 | # Lib3 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `lib3` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:lib3, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/lib/lib3.ex: -------------------------------------------------------------------------------- 1 | defmodule Lib3 do 2 | 3 | defimplEx Float, i when is_float(f), for: Bloop.Bloop do 4 | def get(f), do: {:float, f} 5 | def get_with_fallback(f), do: {:float, f} 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib3.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lib3, 7 | version: "0.1.0", 8 | elixir: "~> 1.17-dev", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | {:protocol_ex, path: "../../../.."}, 27 | {:lib1, in_umbrella: true}, 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/test/lib3_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lib3Test do 2 | use ExUnit.Case 3 | doctest Lib3 4 | 5 | test "greets the world" do 6 | assert Lib3.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/apps/lib3/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your umbrella 2 | # and **all applications** and their dependencies with the 3 | # help of the Config module. 4 | # 5 | # Note that all applications in your umbrella share the 6 | # same configuration and dependencies, which is why they 7 | # all use the same configuration file. If you want different 8 | # configurations or dependencies per app, it is best to 9 | # move said applications out of the umbrella. 10 | import Config 11 | 12 | # Sample configuration: 13 | # 14 | # config :logger, :console, 15 | # level: :info, 16 | # format: "$date $time [$level] $metadata$message\n", 17 | # metadata: [:user_id] 18 | # 19 | -------------------------------------------------------------------------------- /tests_extra/umbrella_test/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule UmbrellaTest.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "umbrella_test", 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Dependencies listed here are available only for this 15 | # project and cannot be accessed from applications inside 16 | # the apps folder. 17 | # 18 | # Run "mix help deps" for examples and options. 19 | defp deps do 20 | [] 21 | end 22 | end 23 | --------------------------------------------------------------------------------