Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
StephaneRob committed Jul 14, 2017
1 parent 0d90cda commit 8ed1b5a
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The directory Mix will write compiled artifacts to.
/_build

# If you run "mix test --cover", coverage assets end up here.
/cover

# The directory Mix downloads your dependencies sources to.
/deps

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Ancestry

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ancestry` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[{:ancestry, "~> 0.1.0"}]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/ancestry](https://hexdocs.pm/ancestry).

30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :ancestry, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:ancestry, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
import_config "test.exs"
13 changes: 13 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use Mix.Config

# Print only warnings and errors during test
config :logger, level: :info

config :ancestry,
ecto_repos: [Ancestry.TestProject.Repo]

config :ancestry, Ancestry.TestProject.Repo,
adapter: Ecto.Adapters.Postgres,
pool: Ecto.Adapters.SQL.Sandbox,
database: "ancestry_test",
hostname: "localhost"
182 changes: 182 additions & 0 deletions lib/ancestry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
defmodule Ancestry do
defmacro __using__(options) do
quote do

import Ecto
import Ecto.Query

alias Ecto.Multi

options = unquote(options)
@model options[:model] || __MODULE__
@app options[:app] || [@model |> Module.split |> List.first] |> Module.concat
@repo options[:repo] || @app |> Module.concat("Repo")
@orphan_stategy options[:orphan_strategy] || :destroy


def delete(model) do
multi =
Multi.new
|> Multi.delete(:delete_model, model)
|> Multi.run(:orphan, __MODULE__, :apply_orphan_strategy, [])
@repo.transaction(multi)
end

# ---- Callbacks
def apply_orphan_strategy(_, model) do
if model.id do
case @orphan_stategy do
:rootify ->
rootify_children(model)
:destroy ->
destroy_children(model)
:adopt ->
adopt_children(model)
end
end
{:ok, model}
end

defp rootify_children(model) do
end

defp destroy_children(model) do
end

defp adopt_children(model) do
end

# ---- Root
def roots do
query = from u in @model,
where: is_nil(u.ancestry) or u.ancestry == "",
order_by: u.inserted_at
@repo.all(query)
end

def root?(model) do
case model.ancestry do
"" -> true
nil -> true
_ -> false
end
end

# ---- Ancestors
def ancestor_ids(model) do
model.ancestry
|> parse_ancestry_column
end

def ancestors(model) do
ids = ancestor_ids(model)
case ancestor_ids(model) do
nil -> nil
ancestors ->
query = from u in @model,
where: u.id in ^ids,
order_by: u.inserted_at
@repo.all(query)
end
end

# ---- Parent
def parent_id(model) do
case ancestor_ids(model) do
nil -> nil
ancestors ->
ancestors
|> List.last
end
end

def parent(model) do
case parent_id(model) do
nil -> nil
id ->
@repo.get!(@model, id)
end
end

def set_parent(model, parent) do
model
|> Ecto.Changeset.change(ancestry: child_ancestry(parent))
end
def set_parent!(model, parent) do
set_parent(model, parent)
|> @repo.update
end

# ---- Children
def children(model) do
query = from u in @model,
where: u.ancestry == ^child_ancestry(model),
order_by: u.inserted_at
@repo.all(query)
end

def child_ids(model) do
for child <- children(model), do: Map.get(child, :id)
end

def children?(model) do
(children(model)|> length) > 0
end

# ---- Siblings
def siblings(model) do
query = from u in @model,
where: u.ancestry == ^"#{model.ancestry}",
order_by: u.inserted_at
@repo.all(query)
end

def sibling_ids(model) do
for sibling <- siblings(model), do: Map.get(sibling, :id)
end

def siblings?(model) do
(siblings(model) |> length) > 0
end

# ---- Descendants
def descendants_ids(model) do
query_string = case root?(model) do
true -> "#{model.id}"
false -> "#{model.ancestry}/#{model.id}"
end
query = from u in @model,
where: like(u.ancestry, ^"#{query_string}/%"),
order_by: u.inserted_at
@repo.all(query)
end

def descendants(model) do
ids = descendants_ids(model)
case descendants_ids(model) do
nil -> nil
desce ->
query = from u in @model,
where: u.id in ^ids,
order_by: u.inserted_at
@repo.all(query)
end
end

defp child_ancestry(model) do
case root?(model) do
true -> "#{model.id}"
false -> "#{model.ancestry}/#{model.id}"
end
end

defp parse_ancestry_column(nil), do: nil
defp parse_ancestry_column(ancestry) do
ancestry
|> String.split("/")
|> Enum.map(fn(x) -> String.to_integer(x) end)
end

end
end
end
30 changes: 30 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Ancestry.Mixfile do
use Mix.Project

def project do
[app: :ancestry,
version: "0.1.0.alpha",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
description: "Ancestry for Ecto.",
package: [
licenses: ["MIT"],
links: %{"Github" => "https://github.com/StephaneRob/ancestry"},
maintainers: ["Stéphane ROBINO"]
],
deps: deps()]
end

def application do
# Specify extra applications you'll use from Erlang/Elixir
[extra_applications: [:logger]]
end

defp deps do
[
{:ecto, "~> 2.1"},
{:postgrex, "~> 0.13", only: :test},
]
end
end
6 changes: 6 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.4.0", "fac965ce71a46aab53d3a6ce45662806bdd708a4a95a65cde8a12eb0124a1333", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}}
22 changes: 22 additions & 0 deletions test/ancestry/ancestors_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Ancestry.Test.AncestorsTest do
use ExUnit.Case
doctest Ancestry

alias Ancestry.TestProject
alias Ancestry.TestProject.{Repo, Page}

setup do
TestProject.Helpers.cleanup
:ok
end

test "get ancestor_ids" do
page = Repo.get(Page, 3)
assert Page.ancestor_ids(page) == [1, 2]
end

test "get all ancestor" do
page = Repo.get(Page, 3)
assert length(Page.ancestors(page)) == 2
end
end
32 changes: 32 additions & 0 deletions test/ancestry/children_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Ancestry.Test.ChildrenTest do
use ExUnit.Case
doctest Ancestry

alias Ancestry.TestProject
alias Ancestry.TestProject.{Repo, Page}

setup do
TestProject.Helpers.cleanup
:ok
end

test "get children" do
page = Repo.get(Page, 3)
assert length(Page.children(page)) == 1
page = Repo.get(Page, 1)
assert length(Page.children(page)) == 2
end

test "get child ids" do
page = Repo.get(Page, 1)
assert Page.child_ids(page) == [2, 6]
end

test "Check if page has children" do
page = Repo.get(Page, 1)
assert Page.children?(page) == true

page = Repo.get(Page, 4)
assert Page.children?(page) == false
end
end
24 changes: 24 additions & 0 deletions test/ancestry/roots_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Ancestry.Test.RootsTest do
use ExUnit.Case
doctest Ancestry

alias Ancestry.TestProject
alias Ancestry.TestProject.{Page, Repo}

setup do
TestProject.Helpers.cleanup
:ok
end

test "get all roots elements" do
roots = Page.roots
assert length(roots) == 2
end

test "Check if page is root" do
page = Repo.get(Page, 1)
page2 = Repo.get(Page, 2)
assert Page.root?(page) == true
assert Page.root?(page2) == false
end
end
Loading

0 comments on commit 8ed1b5a

Please sign in to comment.