├── .gitignore ├── .travis.yml ├── README.md ├── config └── config.exs ├── lib ├── pipe_here.ex └── pipe_here │ └── macro.ex ├── mix.exs ├── mix.lock └── test ├── pipe_here_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | /doc 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.2 4 | - 1.3.4 5 | - 1.4.0 6 | otp_release: 7 | - 18.0 8 | - 19.0 9 | sudo: false 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PipeHere 2 | [![help maintain this lib](https://img.shields.io/badge/looking%20for%20maintainer-DM%20%40vborja-663399.svg)](https://twitter.com/vborja) 3 | 4 | 5 | An Elixir macro for easily piping arguments at any position. 6 | 7 | ## Usage 8 | 9 | ```elixir 10 | import PipeHere 11 | ``` 12 | 13 | The `pipe_here` macro lets you specify the argument position 14 | while piping values by using the `_` placeholder. 15 | 16 | For example: 17 | 18 | ```elixir 19 | pipe_here( 2 |> x(1, _, 3) ) 20 | 21 | # expands to 22 | 23 | x(1, 2, 3) 24 | ``` 25 | 26 | The `pipe_here` macro can also be used at the end of a pipe: 27 | 28 | ```elixir 29 | 2 |> x(1, _, 3) |> pipe_here 30 | ``` 31 | 32 | Note that while you can do stuff like: 33 | 34 | ```elixir 35 | a |> b(1, _) |> c |> d.(2, _, 3) |> pipe_here 36 | ``` 37 | 38 | every member of the pipe can at most have just one `_` placeholder. 39 | 40 | 41 | ## Installation 42 | 43 | [Available in Hex](https://hex.pm/packages/pipe_here), the package can be installed as: 44 | 45 | 1. Add pipe_here to your list of dependencies in `mix.exs`: 46 | 47 | ```elixir 48 | def deps do 49 | [{:pipe_here, "~> 1.0.0"}] 50 | end 51 | ``` 52 | 53 | ## Is it any good? 54 | 55 | [Yes](https://news.ycombinator.com/item?id=3067434) 56 | 57 | ##### マクロス Makurosu 58 | 59 | [[Elixir macros](https://github.com/h4cc/awesome-elixir#macros),] The things I do for beautiful code 60 | ― George Martin, Game of Thrones 61 | 62 | [#myelixirstatus](https://twitter.com/hashtag/myelixirstatus?src=hash) 63 | [#FridayLiterally](http://futurice.com/blog/friday-literally) 64 | -------------------------------------------------------------------------------- /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 | use Mix.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 for your application as: 12 | # 13 | # config :pipe_here, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:pipe_here, :key) 18 | # 19 | # Or 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 | -------------------------------------------------------------------------------- /lib/pipe_here.ex: -------------------------------------------------------------------------------- 1 | defmodule PipeHere do 2 | 3 | defmacro pipe_here(x) do 4 | x |> PipeHere.Macro.piped |> PipeHere.Macro.unpipe |> rewrite_pipe 5 | end 6 | 7 | defp rewrite_pipe([x]), do: x 8 | defp rewrite_pipe([a | rest]) do 9 | if Enum.any?(rest, &call_with_placeholder?/1) do 10 | rest |> Enum.map(&rewrite_call/1) |> List.insert_at(0, a) |> Enum.reduce(&pipe/2) 11 | else 12 | [a | rest] |> Enum.reduce(&PipeHere.Macro.rpipe/2) 13 | end 14 | end 15 | 16 | defp rewrite_call(call = {a, l, args = [_ | _]}) do 17 | idx = Enum.find_index(args, &placeholder?/1) 18 | if idx == nil do 19 | {call, 0} 20 | else 21 | {{a, l, List.delete_at(args, idx)}, idx} 22 | end 23 | end 24 | defp rewrite_call(call), do: {call, 0} 25 | 26 | defp pipe({b, idx}, a) do 27 | Macro.pipe(a, b, idx) 28 | end 29 | 30 | defp call_with_placeholder?({_, _, args = [_ | _]}) do 31 | Enum.any?(args, &placeholder?/1) 32 | end 33 | defp call_with_placeholder?(_), do: false 34 | 35 | 36 | defp placeholder?({:_, _, x}) when is_atom(x), do: true 37 | defp placeholder?(_), do: false 38 | 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/pipe_here/macro.ex: -------------------------------------------------------------------------------- 1 | defmodule PipeHere.Macro do 2 | 3 | @moduledoc false 4 | 5 | # already piped code 6 | def piped(code = {:|>, _, _}), do: code 7 | 8 | # non calls 9 | def piped(code = {_, _, nil}), do: code 10 | 11 | # local functions 12 | def piped(code = {_, _, []}), do: code 13 | 14 | # tuple literals 15 | def piped(code = {:{}, _, _}), do: code 16 | 17 | # handle struct literals %Foo{} 18 | def piped(code = {:%, _, [_, {:%{}, _, _}]}) do 19 | code 20 | end 21 | 22 | def piped({call, ctx, [arg | args]}) do 23 | {:|>, [], [piped(arg), {call, ctx, args}]} 24 | end 25 | 26 | def piped(code), do: code 27 | 28 | def unpipe(piped_code) do 29 | piped_code |> Macro.unpipe |> Enum.map(fn {x,0} -> x end) 30 | end 31 | 32 | def rpipe(a, b) do 33 | Macro.pipe(b, a, 0) 34 | end 35 | 36 | 37 | end 38 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PipeHere.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pipe_here, 6 | version: "1.0.1", 7 | elixir: "~> 1.2", 8 | description: description(), 9 | package: package(), 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps()] 13 | end 14 | 15 | # Configuration for the OTP application 16 | # 17 | # Type "mix help compile.app" for more information 18 | def application do 19 | [applications: [:logger]] 20 | end 21 | 22 | def description do 23 | """ 24 | An Elixir macro for easily piping arguments at any position. 25 | """ 26 | end 27 | 28 | defp package do 29 | [files: ["lib", "mix.exs", "README*"], 30 | maintainers: ["Victor Borja"], 31 | licenses: ["Apache 2.0"], 32 | links: %{"GitHub" => "https://github.com/vic/pipe_here"}] 33 | end 34 | 35 | # Dependencies can be Hex packages: 36 | # 37 | # {:mydep, "~> 0.3.0"} 38 | # 39 | # Or git/path repositories: 40 | # 41 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 42 | # 43 | # Type "mix help deps" for more examples and options 44 | defp deps do 45 | [{:ex_doc, ">= 0.0.0", only: :dev}] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 3 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 4 | "mix_test_watch": {:hex, :mix_test_watch, "0.2.5", "63a34de89f549637f401c7a27598140e0a5a58f5997741993e1016babf134c7b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}} 5 | -------------------------------------------------------------------------------- /test/pipe_here_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PipeHereTest do 2 | use ExUnit.Case 3 | doctest PipeHere 4 | 5 | import PipeHere.Assertions 6 | import PipeHere 7 | 8 | test "non-piped term expands to itself" do 9 | a = quote do 10 | a |> pipe_here 11 | end 12 | b = quote do 13 | a 14 | end 15 | assert_expands_to a, b, __ENV__ 16 | assert :foo == :foo |> pipe_here 17 | end 18 | 19 | test "piped term expands to itself" do 20 | a = quote do 21 | a |> b |> pipe_here 22 | end 23 | b = quote do 24 | b(a) 25 | end 26 | assert_expands_to a, b, __ENV__ 27 | end 28 | 29 | test "call without placeholder expands to itself" do 30 | a = quote do 31 | a |> b(1) |> pipe_here 32 | end 33 | b = quote do 34 | b(a, 1) 35 | end 36 | assert_expands_to a, b, __ENV__ 37 | end 38 | 39 | test "call with placeholder expands to pipe" do 40 | a = quote do 41 | a |> b(_) |> pipe_here 42 | end 43 | b = quote do 44 | b(a) 45 | end 46 | assert_expands_to a, b, __ENV__ 47 | end 48 | 49 | test "call with placeholder at second idx expands to pipe" do 50 | a = quote do 51 | a |> b(1, _) |> pipe_here 52 | end 53 | b = quote do 54 | b(1, a) 55 | end 56 | assert_expands_to a, b, __ENV__ 57 | end 58 | 59 | test "remote call with placeholder expands to pipe" do 60 | a = quote do 61 | a |> M.b(_) |> pipe_here 62 | end 63 | b = quote do 64 | M.b(a) 65 | end 66 | assert_expands_to a, b, __ENV__ 67 | end 68 | 69 | test "remote call with placeholder at second idx expands to pipe" do 70 | a = quote do 71 | a |> M.b(1, _) |> pipe_here 72 | end 73 | b = quote do 74 | M.b(1, a) 75 | end 76 | assert_expands_to a, b, __ENV__ 77 | end 78 | 79 | test "replaces placeholder in pipe" do 80 | a = quote do 81 | a |> b(1, _, 2) |> M.c(3, _) |> pipe_here 82 | end 83 | b = quote do 84 | M.c(3, b(1, a, 2)) 85 | end 86 | assert_expands_to a, b, __ENV__ 87 | end 88 | 89 | test "fncall with placeholder expands to pipe" do 90 | a = quote do 91 | a |> b.(_) |> pipe_here 92 | end 93 | b = quote do 94 | b.(a) 95 | end 96 | assert_expands_to a, b, __ENV__ 97 | end 98 | 99 | test "remote call with ref and placeholder expands to pipe" do 100 | a = quote do 101 | a |> M.b(&x/1, _) |> pipe_here 102 | end 103 | b = quote do 104 | M.b(&x/1, a) 105 | end 106 | assert_expands_to a, b, __ENV__ 107 | end 108 | 109 | test "mixed with non-placeholder calls" do 110 | a = quote do 111 | a |> b(1, _) |> c |> d(2, _, 3) |> e.(4) |> pipe_here 112 | end 113 | b = quote do 114 | e.(d(2, c(b(1, a)) ,3), 4) 115 | end 116 | assert_expands_to a, b, __ENV__ 117 | end 118 | 119 | end 120 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule PipeHere.Assertions do 4 | use ExUnit.Case 5 | 6 | def assert_expands_to(a, b, env) do 7 | as = Macro.to_string(a) 8 | bs = Macro.to_string(b) 9 | 10 | x = Macro.expand_once(a, env) 11 | xs = Macro.to_string(x) 12 | 13 | cond do 14 | xs == bs -> assert(a) 15 | xs == as -> 16 | flunk(""" 17 | Expected 18 | 19 | #{Macro.to_string(a)} 20 | 21 | to expand into 22 | 23 | #{Macro.to_string(b)} 24 | """) 25 | :else -> 26 | assert_expands_to(x, b, env) 27 | end 28 | end 29 | end 30 | --------------------------------------------------------------------------------