├── lib ├── ecto_ltree.ex └── ecto_ltree │ ├── postgrex │ ├── lquery.ex │ └── ltree.ex │ ├── functions.ex │ └── label_tree.ex ├── .formatter.exs ├── test ├── support │ ├── types.ex │ ├── app.ex │ ├── context.ex │ ├── repo.ex │ ├── item.ex │ └── ecto_case.ex ├── test_helper.exs └── ecto_label_tree │ ├── ecto_test.exs │ ├── label_tree_test.exs │ ├── postgrex_test.exs │ └── functions_test.exs ├── priv └── test_repo │ └── migrations │ ├── 20180125154813_create_extension_ltree.exs │ └── 20180125160508_create_items.exs ├── config └── config.exs ├── .gitignore ├── LICENSE.md ├── mix.exs ├── .github └── workflows │ └── main.yml ├── README.md ├── mix.lock └── .credo.exs /lib/ecto_ltree.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/support/types.ex: -------------------------------------------------------------------------------- 1 | Postgrex.Types.define( 2 | EctoLtree.TestTypes, 3 | [EctoLtree.Postgrex.Lquery, EctoLtree.Postgrex.Ltree] ++ Ecto.Adapters.Postgres.extensions() 4 | ) 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | {:ok, _} = Application.ensure_all_started(:postgrex) 2 | {:ok, _} = Application.ensure_all_started(:ecto) 3 | EctoLtree.TestApp.start(:normal, []) 4 | 5 | ExUnit.start() 6 | 7 | Ecto.Adapters.SQL.Sandbox.mode(EctoLtree.TestRepo, :manual) 8 | -------------------------------------------------------------------------------- /priv/test_repo/migrations/20180125154813_create_extension_ltree.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.TestRepo.Migrations.CreateExtensionLtree do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute("CREATE EXTENSION ltree", 6 | "DROP EXTENSION ltree") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/support/app.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.TestApp do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | {EctoLtree.TestRepo, []} 8 | ] 9 | 10 | Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/test_repo/migrations/20180125160508_create_items.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.TestRepo.Migrations.CreateItems do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:items) do 6 | add :path, :ltree 7 | end 8 | 9 | create index(:items, [:path], using: :gist) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/support/context.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.TestContext do 2 | @moduledoc """ 3 | Test Context module 4 | """ 5 | 6 | alias EctoLtree.Item 7 | alias EctoLtree.TestRepo 8 | 9 | def create_item(path) do 10 | %Item{} 11 | |> Item.changeset(%{path: path}) 12 | |> TestRepo.insert() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/support/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.TestRepo do 2 | @moduledoc """ 3 | Test Repo module 4 | """ 5 | use Ecto.Repo, 6 | otp_app: :ecto_ltree, 7 | adapter: Ecto.Adapters.Postgres 8 | 9 | def config_postgrex do 10 | config() 11 | |> Keyword.take([:hostname, :port, :database, :username, :password, :types]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/item.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.Item do 2 | @moduledoc """ 3 | Item schema module 4 | """ 5 | use Ecto.Schema 6 | import Ecto.Changeset 7 | alias EctoLtree.LabelTree, as: Ltree 8 | 9 | schema "items" do 10 | field(:path, Ltree) 11 | end 12 | 13 | def changeset(item, params \\ %{}) do 14 | item 15 | |> cast(params, [:path]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | config :ecto_ltree, ecto_repos: [EctoLtree.TestRepo] 5 | 6 | config :ecto_ltree, EctoLtree.TestRepo, 7 | adapter: Ecto.Adapters.Postgres, 8 | username: "postgres", 9 | password: "postgres", 10 | database: "ecto_ltree_test", 11 | hostname: "localhost", 12 | poolsize: 10, 13 | pool: Ecto.Adapters.SQL.Sandbox, 14 | types: EctoLtree.TestTypes 15 | 16 | config :logger, level: :warn 17 | end 18 | -------------------------------------------------------------------------------- /test/ecto_label_tree/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.EctoTest do 2 | use EctoLtree.EctoCase, async: true 3 | alias EctoLtree.Item 4 | alias EctoLtree.TestContext 5 | 6 | describe "Ecto integration" do 7 | test "can insert record" do 8 | assert {:ok, _schema} = TestContext.create_item("this.is.the.one") 9 | end 10 | 11 | test "can load record" do 12 | TestContext.create_item("this.is.the.one") 13 | 14 | assert 0 < Repo.one(from(i in Item, select: count(i.id))) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.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 | # Ignore package tarball (built via "mix hex.build"). 23 | ecto_label_tree-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test/ecto_label_tree/label_tree_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.LabelTreeTest do 2 | use EctoLtree.EctoCase, async: true 3 | alias EctoLtree.LabelTree, as: Ltree 4 | 5 | describe "Ltree.cast/1" do 6 | test "top label" do 7 | {:ok, result} = Ltree.cast("top") 8 | assert %Ltree{} = result 9 | assert 1 = length(result.labels) 10 | end 11 | 12 | test "more than one label" do 13 | {:ok, result} = Ltree.cast("top.countries.south_america") 14 | assert %Ltree{} = result 15 | assert 3 = length(result.labels) 16 | end 17 | 18 | test "label chars" do 19 | assert {:ok, %Ltree{}} = Ltree.cast("411_valid_chars") 20 | assert :error == Ltree.cast("invalid chars") 21 | end 22 | 23 | test "label length" do 24 | long_label = String.duplicate("long", 64) 25 | assert {:ok, %Ltree{}} = Ltree.cast(long_label) 26 | 27 | # 260 28 | too_long_label = String.duplicate("too_long", 33) 29 | assert :error == Ltree.cast(too_long_label) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/ecto_ltree/postgrex/lquery.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.Postgrex.Lquery do 2 | @moduledoc """ 3 | This module provides the necessary functions to encode and decode PostgreSQL’s `lquery` data type to and from Elixir values. 4 | Implements the Postgrex.Extension behaviour. 5 | """ 6 | 7 | @behaviour Postgrex.Extension 8 | 9 | @impl true 10 | def init(opts) do 11 | Keyword.get(opts, :decode_copy, :copy) 12 | end 13 | 14 | @impl true 15 | def matching(_state), do: [type: "lquery"] 16 | 17 | @impl true 18 | def format(_state), do: :text 19 | 20 | @impl true 21 | def encode(_state) do 22 | quote do 23 | bin when is_binary(bin) -> 24 | [<> | bin] 25 | end 26 | end 27 | 28 | @impl true 29 | def decode(:reference) do 30 | quote do 31 | <> -> 32 | bin 33 | end 34 | end 35 | 36 | def decode(:copy) do 37 | quote do 38 | <> -> 39 | :binary.copy(bin) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 Jose Miguel Rivero Bruno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/ecto_label_tree/postgrex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.PostgrexTest do 2 | use ExUnit.Case 3 | 4 | setup do 5 | {:ok, pid} = Postgrex.start_link(EctoLtree.TestRepo.config_postgrex()) 6 | {:ok, [pid: pid]} 7 | end 8 | 9 | test "insert item", context do 10 | pid = context[:pid] 11 | path = "this.is.the.path" 12 | {:ok, _} = Postgrex.query(pid, "INSERT INTO items (path) VALUES ($1)", [path]) 13 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM items", []) 14 | [[_, result_path]] = result.rows 15 | 16 | assert path == result_path 17 | assert {:ok, _} = Postgrex.query(pid, "TRUNCATE TABLE items", []) 18 | end 19 | 20 | test "query item", context do 21 | pid = context[:pid] 22 | root = "this.is" 23 | path = root <> ".the.path" 24 | {:ok, _} = Postgrex.query(pid, "INSERT INTO items (path) VALUES ($1)", [path]) 25 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM items WHERE path <@ $1;", [root]) 26 | [[_, result_path]] = result.rows 27 | 28 | assert path == result_path 29 | assert {:ok, _} = Postgrex.query(pid, "TRUNCATE TABLE items", []) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/ecto_case.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.EctoCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | alias Ecto.Adapters.SQL 18 | 19 | using do 20 | quote do 21 | alias EctoLtree.TestRepo, as: Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import EctoLtree.EctoCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = SQL.Sandbox.checkout(EctoLtree.TestRepo) 32 | 33 | unless tags[:async] do 34 | SQL.Sandbox.mode(EctoLtree.TestRepo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transform changeset errors to a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | def errors_on(changeset) do 49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 50 | Enum.reduce(opts, message, fn {key, value}, acc -> 51 | String.replace(acc, "%{#{key}}", to_string(value)) 52 | end) 53 | end) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ltree, 7 | version: "0.4.0", 8 | elixir: "~> 1.9", 9 | start_permanent: Mix.env() == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | dialyzer: [flags: [:unmatched_returns, :error_handling, :race_conditions, :underspecs]], 12 | description: description(), 13 | package: package(), 14 | aliases: aliases(), 15 | deps: deps(), 16 | name: "EctoLtree", 17 | source_url: "https://github.com/josemrb/ecto_ltree", 18 | docs: [main: "readme", extras: ["README.md"]] 19 | ] 20 | end 21 | 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | def elixirc_paths(:test), do: ["lib", "test/support"] 29 | def elixirc_paths(_), do: ["lib"] 30 | 31 | defp description() do 32 | "A library that provides the necessary modules to support the PostgreSQL’s `ltree` data type with Ecto." 33 | end 34 | 35 | defp aliases do 36 | [ 37 | "code.qa": ["credo --strict", "format --check-formatted"], 38 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 39 | ] 40 | end 41 | 42 | defp deps do 43 | [ 44 | {:ecto, "~> 3.2"}, 45 | {:postgrex, ">= 0.0.0"}, 46 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 47 | {:ex_doc, "~> 0.22", only: :dev, runtime: false}, 48 | {:dialyxir, "~> 1.1", only: :dev, runtime: false}, 49 | {:ecto_sql, "~> 3.2", only: :test} 50 | ] 51 | end 52 | 53 | defp package() do 54 | [ 55 | licenses: ["MIT"], 56 | maintainers: ["Jose Miguel Rivero Bruno (@josemrb)"], 57 | links: %{"GitHub" => "https://github.com/josemrb/ecto_ltree"}, 58 | files: ["lib", "priv", "mix.exs", "README*", "LICENSE*"] 59 | ] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/ecto_label_tree/functions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.FunctionsTest do 2 | use EctoLtree.EctoCase, async: true 3 | import EctoLtree.Functions 4 | alias EctoLtree.Item 5 | alias EctoLtree.TestContext 6 | 7 | describe "subltree/3" do 8 | test "returns value" do 9 | {:ok, inserted} = TestContext.create_item("top.sciences.mathematics") 10 | 11 | query = from(item in Item, where: subltree(item.path, 0, 2) == "top.sciences") 12 | 13 | assert [inserted] == query |> Repo.all() 14 | end 15 | end 16 | 17 | describe "subpath/2" do 18 | test "returns value" do 19 | TestContext.create_item("top.sciences.physics") 20 | 21 | query = from(item in Item, select: subpath(item.path, 1)) 22 | 23 | assert ["sciences.physics"] == query |> Repo.all() 24 | end 25 | end 26 | 27 | describe "subpath/3" do 28 | test "returns value" do 29 | {:ok, inserted} = TestContext.create_item("top.sciences.physics") 30 | 31 | query = from(item in Item, where: subpath(item.path, -1, 1) == "physics") 32 | 33 | assert [inserted] == query |> Repo.all() 34 | end 35 | end 36 | 37 | describe "nlevel/1" do 38 | test "returns value" do 39 | TestContext.create_item("top.sciences.physics") 40 | 41 | query = from(item in Item, select: nlevel(item.path)) 42 | 43 | assert [3] == query |> Repo.all() 44 | end 45 | end 46 | 47 | describe "index/2" do 48 | test "returns value" do 49 | end 50 | end 51 | 52 | describe "index/3" do 53 | test "returns value" do 54 | end 55 | end 56 | 57 | describe "text2ltree/1" do 58 | test "returns value" do 59 | end 60 | end 61 | 62 | describe "ltree2text/1" do 63 | test "returns value" do 64 | end 65 | end 66 | 67 | describe "lca/2" do 68 | test "returns value" do 69 | TestContext.create_item("top.sciences.mathematics") 70 | 71 | query = from(item in Item, select: lca(item.path, "top.sciences.physics")) 72 | 73 | assert ["top.sciences"] == query |> Repo.all() 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/ecto_ltree/functions.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.Functions do 2 | @moduledoc """ 3 | This module exposes the `ltree` functions. 4 | For more information see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/static/ltree.html#LTREE-FUNC-TABLE). 5 | """ 6 | 7 | @doc """ 8 | subpath of `ltree` from position start to position end-1 (counting from 0). 9 | """ 10 | defmacro subltree(ltree, start, finish) do 11 | quote do: fragment("SUBLTREE(?, ?, ?)", unquote(ltree), unquote(start), unquote(finish)) 12 | end 13 | 14 | @doc """ 15 | subpath of `ltree` starting at position offset, extending to end of path. 16 | If offset is negative, subpath starts that far from the end of the path. 17 | """ 18 | defmacro subpath(ltree, offset) do 19 | quote do: fragment("SUBPATH(?, ?)", unquote(ltree), unquote(offset)) 20 | end 21 | 22 | @doc """ 23 | subpath of `ltree` starting at position offset, length len. 24 | If offset is negative, subpath starts that far from the end of the path. 25 | If len is negative, leaves that many labels off the end of the path. 26 | """ 27 | defmacro subpath(ltree, offset, len) do 28 | quote do: fragment("SUBPATH(?, ?, ?)", unquote(ltree), unquote(offset), unquote(len)) 29 | end 30 | 31 | @doc """ 32 | number of labels in path. 33 | """ 34 | defmacro nlevel(ltree) do 35 | quote do: fragment("NLEVEL(?)", unquote(ltree)) 36 | end 37 | 38 | @doc """ 39 | position of first occurrence of b in a; -1 if not found. 40 | """ 41 | defmacro index(a, b) do 42 | quote do: fragment("INDEX(?, ?)", unquote(a), unquote(b)) 43 | end 44 | 45 | @doc """ 46 | position of first occurrence of b in a, searching starting at offset; negative offset means start -offset labels from the end of the path. 47 | """ 48 | defmacro index(a, b, offset) do 49 | quote do: fragment("INDEX(?, ?, ?)", unquote(a), unquote(b), unquote(offset)) 50 | end 51 | 52 | @doc """ 53 | cast `text` to `ltree`. 54 | """ 55 | defmacro text2ltree(text) do 56 | quote do: fragment("TEXT2LTREE(?)", unquote(text)) 57 | end 58 | 59 | @doc """ 60 | cast `ltree` to `text`. 61 | """ 62 | defmacro ltree2text(ltree) do 63 | quote do: fragment("LTREE2TEXT(?)", unquote(ltree)) 64 | end 65 | 66 | @doc """ 67 | lowest common ancestor. 68 | """ 69 | defmacro lca(a, b) do 70 | quote do: fragment("LCA(?, ?)", unquote(a), unquote(b)) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | 8 | jobs: 9 | lint: 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | MIX_ENV: dev 13 | name: Lint 14 | strategy: 15 | matrix: 16 | os: ["ubuntu-20.04"] 17 | elixir: ["1.13"] 18 | otp: ["24"] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: erlef/setup-beam@v1 22 | with: 23 | otp-version: ${{ matrix.otp }} 24 | elixir-version: ${{ matrix.elixir }} 25 | - uses: actions/cache@v2 26 | with: 27 | path: deps 28 | key: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_${{ hashFiles('**/mix.lock') }} 29 | restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ 30 | - run: mix deps.get 31 | - run: mix deps.compile 32 | - run: mix format --check-formatted 33 | - run: mix deps.unlock --check-unused 34 | - run: mix credo --strict --all 35 | - name: Cache dialyzer 36 | uses: actions/cache@v2 37 | with: 38 | path: priv/plts 39 | key: plts-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }} 40 | - run: mix dialyzer 41 | 42 | test: 43 | runs-on: ${{ matrix.os }} 44 | name: Test Elixir ${{ matrix.elixir }}, OTP ${{ matrix.otp }}, OS ${{ matrix.os }} 45 | env: 46 | MIX_ENV: test 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: ["ubuntu-20.04"] 51 | elixir: ["1.13", "1.12", "1.11"] 52 | otp: ["24", "23", "22"] 53 | pg: ["10", "11", "12"] 54 | services: 55 | pg: 56 | image: postgres:${{ matrix.pg }} 57 | env: 58 | POSTGRES_USER: postgres 59 | POSTGRES_PASSWORD: postgres 60 | POSTGRES_DB: postgres 61 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 62 | ports: ["5432:5432"] 63 | steps: 64 | - uses: actions/checkout@v2 65 | - uses: erlef/setup-beam@v1 66 | with: 67 | otp-version: ${{ matrix.otp }} 68 | elixir-version: ${{ matrix.elixir }} 69 | - uses: actions/cache@v2 70 | with: 71 | path: deps 72 | key: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_${{ hashFiles('**/mix.lock') }} 73 | restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ 74 | - run: mix deps.get --only test 75 | - run: mix deps.compile 76 | - run: mix compile 77 | - run: mix test 78 | -------------------------------------------------------------------------------- /lib/ecto_ltree/label_tree.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoLtree.LabelTree do 2 | @moduledoc """ 3 | This module defines the LabelTree struct. 4 | Implements the Ecto.Type behaviour. 5 | 6 | ## Fields 7 | * `labels` 8 | """ 9 | 10 | use Ecto.Type 11 | 12 | alias EctoLtree.LabelTree, as: Ltree 13 | 14 | @type t :: %__MODULE__{ 15 | labels: [String.t()] 16 | } 17 | 18 | defstruct labels: [] 19 | 20 | @labelpath_size_max 2048 21 | 22 | @doc """ 23 | Provides custom casting rules from external data to internal representation. 24 | """ 25 | @spec cast(String.t()) :: {:ok, t} | :error 26 | def cast(string) when is_binary(string) and byte_size(string) <= @labelpath_size_max do 27 | labels_result = 28 | string 29 | |> String.split(".") 30 | |> Enum.map(&cast_label/1) 31 | 32 | if Enum.any?(labels_result, fn i -> i == :error end) do 33 | :error 34 | else 35 | {:ok, %Ltree{labels: Enum.map(labels_result, fn {_k, v} -> v end)}} 36 | end 37 | end 38 | 39 | def cast(%Ltree{} = struct) do 40 | {:ok, struct} 41 | end 42 | 43 | def cast(_), do: :error 44 | 45 | @label_size_max 256 46 | @label_regex ~r/[A-Za-z0-9_]{1,256}/ 47 | 48 | @spec cast_label(String.t()) :: {:ok, String.t()} | :error 49 | defp cast_label(string) when is_binary(string) and byte_size(string) <= @label_size_max do 50 | string_length = String.length(string) 51 | 52 | case Regex.run(@label_regex, string, return: :index) do 53 | [{0, last}] when last == string_length -> 54 | {:ok, string} 55 | 56 | _ -> 57 | :error 58 | end 59 | end 60 | 61 | defp cast_label(_), do: :error 62 | 63 | @doc """ 64 | From internal representation to database. 65 | """ 66 | @spec dump(t) :: {:ok, String.t()} | :error 67 | def dump(%Ltree{} = label_tree) do 68 | {:ok, decode(label_tree)} 69 | end 70 | 71 | def dump(_), do: :error 72 | 73 | @spec decode(t) :: String.t() 74 | def decode(%Ltree{} = label_tree) do 75 | Enum.join(label_tree.labels, ".") 76 | end 77 | 78 | @doc """ 79 | From database to internal representation. 80 | """ 81 | @spec load(String.t()) :: {:ok, t} | :error 82 | def load(labelpath) when is_binary(labelpath) do 83 | {:ok, %Ltree{labels: labelpath |> String.split(".")}} 84 | end 85 | 86 | def load(_), do: :error 87 | 88 | @doc """ 89 | Returns the underlying schema type. 90 | """ 91 | @spec type() :: :ltree 92 | def type, do: :ltree 93 | end 94 | 95 | defimpl String.Chars, for: EctoLtree.LabelTree do 96 | def to_string(%EctoLtree.LabelTree{} = label_tree), do: EctoLtree.LabelTree.decode(label_tree) 97 | end 98 | -------------------------------------------------------------------------------- /lib/ecto_ltree/postgrex/ltree.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Eric Meadows-Jönsson 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | # use this file except in compliance with the License. You may obtain a copy of 5 | # the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | 15 | defmodule EctoLtree.Postgrex.Ltree do 16 | @moduledoc """ 17 | This module provides the necessary functions to encode and decode PostgreSQL’s 18 | `ltree` data type to and from Elixir values. 19 | 20 | Implements the Postgrex.Extension behaviour. 21 | """ 22 | 23 | @behaviour Postgrex.Extension 24 | 25 | # It can be memory efficient to copy the decoded binary because a 26 | # reference counted binary that points to a larger binary will be passed 27 | # to the decode/4 callback. Copying the binary can allow the larger 28 | # binary to be garbage collected sooner if the copy is going to be kept 29 | # for a longer period of time. See [`:binary.copy/1`](http://www.erlang.org/doc/man/binary.html#copy-1) for more 30 | # information. 31 | @impl true 32 | def init(opts) do 33 | Keyword.get(opts, :decode_copy, :copy) 34 | end 35 | 36 | # Use this extension when `type` from %Postgrex.TypeInfo{} is "ltree" 37 | @impl true 38 | def matching(_state), do: [type: "ltree"] 39 | 40 | # Use the text format, "ltree" does not have a binary format. 41 | @impl true 42 | def format(_state), do: :text 43 | 44 | # Use quoted expression to encode a string that is the same as 45 | # postgresql's ltree text format. The quoted expression should contain 46 | # clauses that match those of a `case` or `fn`. Encoding matches on the 47 | # value and returns encoded `iodata()`. The first 4 bytes in the 48 | # `iodata()` must be the byte size of the rest of the encoded data, as a 49 | # signed 32bit big endian integer. 50 | @impl true 51 | def encode(_state) do 52 | quote do 53 | bin when is_binary(bin) -> 54 | [<> | bin] 55 | end 56 | end 57 | 58 | # Use quoted expression to decode the data to a string. Decoding matches 59 | # on an encoded binary with the same signed 32bit big endian integer 60 | # length header. 61 | @impl true 62 | def decode(:reference) do 63 | quote do 64 | <> -> 65 | bin 66 | end 67 | end 68 | 69 | def decode(:copy) do 70 | quote do 71 | <> -> 72 | :binary.copy(bin) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EctoLtree 2 | [![Hex Version](https://img.shields.io/hexpm/v/ecto_ltree.svg?style=flat)](https://hex.pm/packages/ecto_ltree) 3 | 4 | A library that provides the necessary modules to support the PostgreSQL’s 5 | `ltree` data type with Ecto. 6 | 7 | ## Quickstart 8 | 9 | ### 1. Add the package to your list of dependencies in `mix.exs` 10 | 11 | #### If you are using Elixir >= v1.7 and Ecto ~> 3.2 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | ... 17 | {:ecto_ltree, "~> 0.3.0"} 18 | ] 19 | end 20 | ``` 21 | 22 | #### If you are using Ecto ~> 3.0 23 | 24 | ```elixir 25 | def deps do 26 | [ 27 | ... 28 | {:ecto_ltree, "~> 0.2.0"} 29 | ] 30 | end 31 | ``` 32 | 33 | #### If you are using Elixir v1.6 and Ecto ~> 2.1 34 | 35 | ```elixir 36 | def deps do 37 | [ 38 | ... 39 | {:ecto_ltree, "~> 0.1.0"} 40 | ] 41 | end 42 | 43 | ``` 44 | ### 2. Define a type module with our custom extensions 45 | 46 | ```elixir 47 | Postgrex.Types.define( 48 | MyApp.PostgresTypes, 49 | [EctoLtree.Postgrex.Lquery, EctoLtree.Postgrex.Ltree] ++ Ecto.Adapters.Postgres.extensions() 50 | ) 51 | ``` 52 | 53 | ### 3. Configure the Repo to use the previously defined type module 54 | 55 | ```elixir 56 | config :my_app, MyApp.Repo, 57 | adapter: Ecto.Adapters.Postgres, 58 | username: "postgres", 59 | password: "postgres", 60 | database: "my_app_dev", 61 | hostname: "localhost", 62 | poolsize: 10, 63 | pool: Ecto.Adapters.SQL.Sandbox, 64 | types: MyApp.PostgresTypes 65 | ``` 66 | 67 | ### 4. Add a migration to enable the `ltree` extension 68 | 69 | ```elixir 70 | defmodule MyApp.Repo.Migrations.CreateExtensionLtree do 71 | use Ecto.Migration 72 | 73 | def change do 74 | execute("CREATE EXTENSION ltree", 75 | "DROP EXTENSION ltree") 76 | end 77 | end 78 | ``` 79 | 80 | ### 5. Add a migration to create your table 81 | 82 | ```elixir 83 | defmodule MyApp.Repo.Migrations.CreateItems do 84 | use Ecto.Migration 85 | 86 | def change do 87 | create table(:items) do 88 | add :path, :ltree 89 | end 90 | 91 | create index(:items, [:path], using: :gist) 92 | end 93 | end 94 | ``` 95 | 96 | ### 6. Define an Ecto Schema 97 | 98 | ```elixir 99 | defmodule MyApp.Item do 100 | use Ecto.Schema 101 | import Ecto.Changeset 102 | alias EctoLtree.LabelTree, as: Ltree 103 | 104 | schema "items" do 105 | field :path, Ltree 106 | end 107 | 108 | def changeset(item, params \\ %{}) do 109 | item 110 | |> cast(params, [:path]) 111 | end 112 | end 113 | ``` 114 | 115 | ### 7. Usage 116 | 117 | ```elixir 118 | iex(1)> alias MyApp.Repo 119 | MyApp.Repo 120 | iex(2)> alias MyApp.Item 121 | MyApp.Item 122 | iex(3)> import Ecto.Query 123 | Ecto.Query 124 | iex(4)> import EctoLtree.Functions 125 | EctoLtree.Functions 126 | iex(5)> Item.changeset(%Item{}, %{path: “1.2.3”}) |> Repo.insert! 127 | %MyApp.Item{ 128 | __meta__: #Ecto.Schema.Metadata<:loaded, “items”>, 129 | id: 1, 130 | path: %EctoLtree.LabelTree{labels: [“1”, “2”, “3”]} 131 | } 132 | iex(6)> from(item in Item, select: nlevel(item.path)) |> Repo.one 133 | 3 134 | ``` 135 | 136 | The documentation can be found at [hexdocs](https://hexdocs.pm/ecto_ltree). 137 | 138 | ## Copyright and License 139 | 140 | Copyright (c) 2018-2019 Jose Miguel Rivero Bruno 141 | 142 | The source code is licensed under [The MIT License (MIT)](LICENSE.md) 143 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, 4 | "credo": {:hex, :credo, "1.6.3", "0a9f8925dbc8f940031b789f4623fc9a0eea99d3eed600fe831e403eb96c6a83", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1167cde00e6661d740fc54da2ee268e35d3982f027399b64d3e2e83af57a1180"}, 5 | "db_connection": {:hex, :db_connection, "2.3.0", "d56ef906956a37959bcb385704fc04035f4f43c0f560dd23e00740daf8028c49", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "dcc082b8f723de9a630451b49fdbd7a59b065c4b38176fb147aaf773574d4520"}, 6 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 7 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 9 | "ecto": {:hex, :ecto, "3.5.3", "64aa70c6a64b8ee6a28ee186083b317b082beac8fed4d55bcc3f23199667a2f3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c481b220bb080e94fd1ab528c3b62bdfdd29188c74aef44fc2b204efa8769532"}, 10 | "ecto_sql": {:hex, :ecto_sql, "3.5.1", "7c03f302caa3c2bbc4f5397281a5d0d8653f246d47c353e3cd46750b16ad310c", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "338150ecb2398f013f98e4a70d5413b70fed4b6383d4f7c400314d315cdf87a9"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 13 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 14 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 15 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 18 | "postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"}, 19 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 20 | } 21 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterFilter, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 137 | 138 | # 139 | ## Warnings 140 | # 141 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 142 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 143 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 144 | {Credo.Check.Warning.IExPry, []}, 145 | {Credo.Check.Warning.IoInspect, []}, 146 | {Credo.Check.Warning.OperationOnSameValues, []}, 147 | {Credo.Check.Warning.OperationWithConstantResult, []}, 148 | {Credo.Check.Warning.RaiseInsideRescue, []}, 149 | {Credo.Check.Warning.SpecWithStruct, []}, 150 | {Credo.Check.Warning.WrongTestFileExtension, []}, 151 | {Credo.Check.Warning.UnusedEnumOperation, []}, 152 | {Credo.Check.Warning.UnusedFileOperation, []}, 153 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 154 | {Credo.Check.Warning.UnusedListOperation, []}, 155 | {Credo.Check.Warning.UnusedPathOperation, []}, 156 | {Credo.Check.Warning.UnusedRegexOperation, []}, 157 | {Credo.Check.Warning.UnusedStringOperation, []}, 158 | {Credo.Check.Warning.UnusedTupleOperation, []}, 159 | {Credo.Check.Warning.UnsafeExec, []} 160 | ], 161 | disabled: [ 162 | # 163 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 164 | 165 | # 166 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 167 | # and be sure to use `mix credo --strict` to see low priority checks) 168 | # 169 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 170 | {Credo.Check.Consistency.UnusedVariableNames, []}, 171 | {Credo.Check.Design.DuplicatedCode, []}, 172 | {Credo.Check.Design.SkipTestWithoutComment, []}, 173 | {Credo.Check.Readability.AliasAs, []}, 174 | {Credo.Check.Readability.BlockPipe, []}, 175 | {Credo.Check.Readability.ImplTrue, []}, 176 | {Credo.Check.Readability.MultiAlias, []}, 177 | {Credo.Check.Readability.NestedFunctionCalls, []}, 178 | {Credo.Check.Readability.SeparateAliasRequire, []}, 179 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 180 | {Credo.Check.Readability.SinglePipe, []}, 181 | {Credo.Check.Readability.Specs, []}, 182 | {Credo.Check.Readability.StrictModuleLayout, []}, 183 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 184 | {Credo.Check.Refactor.ABCSize, []}, 185 | {Credo.Check.Refactor.AppendSingleItem, []}, 186 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 187 | {Credo.Check.Refactor.FilterReject, []}, 188 | {Credo.Check.Refactor.IoPuts, []}, 189 | {Credo.Check.Refactor.MapMap, []}, 190 | {Credo.Check.Refactor.ModuleDependencies, []}, 191 | {Credo.Check.Refactor.NegatedIsNil, []}, 192 | {Credo.Check.Refactor.PipeChainStart, []}, 193 | {Credo.Check.Refactor.RejectFilter, []}, 194 | {Credo.Check.Refactor.VariableRebinding, []}, 195 | {Credo.Check.Warning.LazyLogging, []}, 196 | {Credo.Check.Warning.LeakyEnvironment, []}, 197 | {Credo.Check.Warning.MapGetUnsafePass, []}, 198 | {Credo.Check.Warning.MixEnv, []}, 199 | {Credo.Check.Warning.UnsafeToAtom, []} 200 | 201 | # {Credo.Check.Refactor.MapInto, []}, 202 | 203 | # 204 | # Custom checks can be created using `mix credo.gen.check`. 205 | # 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | --------------------------------------------------------------------------------