├── .gitignore ├── .travis.yml ├── README.md ├── lib ├── red.ex └── red │ ├── client.ex │ ├── edge.ex │ ├── entity.ex │ ├── key.ex │ ├── query.ex │ ├── query │ └── meta.ex │ └── relation.ex ├── mix.exs ├── mix.lock └── test ├── follow_test.exs ├── red_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | erl_crash.dump 6 | *.tar 7 | *.ez 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | matrix: 3 | include: 4 | - otp_release: 19.0 5 | elixir: 1.3.4 6 | - otp_release: 19.3 7 | elixir: 1.3 8 | - otp_release: 19.3 9 | elixir: 1.4 10 | - otp_release: 20.0 11 | elixir: 1.4 12 | - otp_release: 20.0 13 | elixir: 1.5 14 | sudo: false 15 | after_script: 16 | - mix deps.get --only docs 17 | - MIX_ENV=docs mix inch.report 18 | services: 19 | - redis 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Red 2 | === 3 | 4 | [![Hex version](https://img.shields.io/hexpm/v/red.svg "Hex version")](https://hex.pm/packages/red) 5 | [![Build status](https://img.shields.io/travis/rodrigues/red.svg "Build status")](https://travis-ci.org/rodrigues/red) 6 | [![Deps Status](https://beta.hexfaktor.org/badge/all/github/rodrigues/red.svg)](https://beta.hexfaktor.org/github/rodrigues/red) 7 | [![Inline docs](http://inch-ci.org/github/rodrigues/red.svg?branch=master&style=flat)](http://hexdocs.pm/red) 8 | ![Hex downloads](https://img.shields.io/hexpm/dt/red.svg "Hex downloads") 9 | 10 | Store relations between entities using [redis](http://redis.io). 11 | 12 | ## Example: A `follow` system 13 | 14 | ```elixir 15 | import Red 16 | 17 | # @vcr2 -{follow}-> @hex_pm 18 | 19 | {:ok, _} = 20 | "@vcr2" 21 | |> relation(:follow) 22 | |> add!("@hex_pm") 23 | 24 | "@vcr2" |> relation(:follow) |> Enum.at(0) 25 | > "@hex_pm" 26 | 27 | # @vcr2 ===follow===> * 28 | count_following = "@vcr2" |> relation(:follow) |> Enum.count 29 | > 100 30 | 31 | # @vcr2 <===follow=== * 32 | count_followers = "@vcr2" |> relation(:follow, :in) |> Enum.count 33 | > 43 34 | 35 | # jump 10, next 5 36 | "@vcr2" |> relation(:follow) |> offset(10) |> limit(5) |> Enum.to_list 37 | > [] 38 | ``` 39 | -------------------------------------------------------------------------------- /lib/red.ex: -------------------------------------------------------------------------------- 1 | defmodule Red do 2 | alias Red.{Entity, Relation, Edge, Key, Query, Client} 3 | 4 | @moduledoc ~S""" 5 | Provides the main functions to work with Red. 6 | 7 | ## Examples 8 | 9 | iex> {:ok, _} = "@vcr2" |> Red.relation(:follow) |> Red.add!("@hex_pm") 10 | ...> "@vcr2" |> Red.relation(:follow) |> Enum.at(0) 11 | "@hex_pm" 12 | 13 | iex> "@vcr2" |> Red.relation(:follow) |> Red.add!(["@elixirlang", "@elixirphoenix"]) 14 | ...> "@vcr2" |> Red.relation(:follow) |> Enum.count 15 | 2 16 | 17 | iex> {:ok, _} = "@elixirlang" |> Red.relation(:follow) |> Red.add!("@vcr2") 18 | ...> "@vcr2" |> Red.relation(:follow, :in) |> Enum.count 19 | 1 20 | 21 | iex> {:ok, _} = "@vcr2" |> Red.relation(:follow) |> Red.add!(~w(@a @b @c @d @e @f @g @h @i @j @k)) 22 | ...> "@vcr2" |> Red.relation(:follow) |> Red.offset(3) |> Red.limit(5) |> Enum.to_list 23 | ["@h", "@g"] 24 | """ 25 | 26 | @doc ~S""" 27 | Returns the redis key corresponding to queryable passed as argument. 28 | 29 | Anything that qualifies as a `t:Red.Query.queryable_t/0` 30 | type can be an argument. 31 | 32 | ## Examples 33 | 34 | iex> Red.key(1) 35 | "1" 36 | 37 | iex> Red.key({:user, 42}) 38 | "user#42" 39 | 40 | iex> Red.key("user#42") 41 | "user#42" 42 | 43 | iex> Red.key("key") 44 | "key" 45 | 46 | iex> Red.key(user: 42) 47 | "user#42" 48 | 49 | iex> %Red.Entity{class: :user, id: 42} |> Red.key 50 | "user#42" 51 | 52 | iex> {:user, 42} |> Red.relation(:follow, :in) |> Red.key 53 | "user#42:follow:in" 54 | 55 | iex> [user: 42] |> Red.relation(:follow) |> Red.key 56 | "user#42:follow:out" 57 | """ 58 | @spec key(Query.queryable_t) :: String.t 59 | defdelegate key(queryable), to: Key, as: :build 60 | 61 | @doc ~S""" 62 | Builds a `Red.Entity` representation. 63 | 64 | Anything that qualifies as a `t:Red.Entity.conversible_to_entity/0` 65 | type can be an argument. 66 | 67 | ## Examples 68 | 69 | iex> Red.entity(1) 70 | %Red.Entity{id: 1} 71 | 72 | iex> Red.entity({:user, 42}) 73 | %Red.Entity{class: :user, id: 42} 74 | 75 | iex> Red.entity("user#42") 76 | %Red.Entity{class: "user", id: "42"} 77 | 78 | iex> Red.entity("key") 79 | %Red.Entity{id: "key"} 80 | 81 | iex> Red.entity(user: 42) 82 | %Red.Entity{class: :user, id: 42} 83 | 84 | iex> %Red.Entity{id: 1} |> Red.entity 85 | %Red.Entity{id: 1} 86 | """ 87 | @spec entity(Entity.conversible_to_entity) :: Red.Entity.t 88 | defdelegate entity(args), to: Entity, as: :build 89 | 90 | @doc ~S""" 91 | Builds a `Red.Relation` representation. 92 | 93 | ## Examples 94 | 95 | iex> "user#42" |> Red.relation(:follow) 96 | %Red.Relation{ 97 | name: :follow, 98 | direction: :out, 99 | entity: %Red.Entity{class: "user", id: "42"} 100 | } 101 | 102 | iex> {:user, 42} |> Red.relation(:follow, :in) 103 | %Red.Relation{ 104 | name: :follow, 105 | direction: :in, 106 | entity: %Red.Entity{class: :user, id: 42} 107 | } 108 | 109 | iex> "user#42" |> Red.relation(:follow, :out) 110 | %Red.Relation{ 111 | name: :follow, 112 | direction: :out, 113 | entity: %Red.Entity{class: "user", id: "42"} 114 | } 115 | """ 116 | @spec relation(Entity.conversible_to_entity, Relation.name_t, :in | :out) :: Relation.t 117 | def relation(entity, name, direction \\ :out) when direction in [:in, :out] do 118 | %Relation{ 119 | name: name, 120 | direction: direction, 121 | entity: Red.entity(entity) 122 | } 123 | end 124 | 125 | @doc ~S""" 126 | Builds a `Red.Edge` representation. 127 | 128 | ## Examples 129 | 130 | iex> {:user, 42} |> Red.relation(:follow) |> Red.edge({:user, 21}) 131 | %Red.Edge{ 132 | relation: %Red.Relation{ 133 | name: :follow, 134 | direction: :out, 135 | entity: %Red.Entity{class: :user, id: 42} 136 | }, 137 | target: %Red.Entity{class: :user, id: 21} 138 | } 139 | """ 140 | @spec edge(Relation.t, Entity.conversible_to_entity) :: Edge.t 141 | def edge(%Relation{} = relation, target_entity) do 142 | %Edge{ 143 | relation: relation, 144 | target: Red.entity(target_entity) 145 | } 146 | end 147 | 148 | @doc ~S""" 149 | Builds a `Red.Query` representation from a `Red.Relation` representation. 150 | 151 | ## Examples 152 | 153 | iex> {:user, 42} |> Red.relation(:follow) |> Red.query 154 | %Red.Query{ 155 | queryable: %Red.Relation{ 156 | name: :follow, 157 | direction: :out, 158 | entity: %Red.Entity{class: :user, id: 42} 159 | }, 160 | meta: %Red.Query.Meta{ 161 | limit: -1, 162 | offset: 0 163 | } 164 | } 165 | """ 166 | @spec query(Relation.t) :: Query.t 167 | def query(%Relation{} = relation), do: %Query{queryable: relation} 168 | 169 | @doc ~S""" 170 | Adds a page limit to the query being mounted. 171 | 172 | ## Examples 173 | 174 | iex> {:user, 42} |> Red.relation(:like) |> Red.limit(80) 175 | %Red.Query{ 176 | queryable: %Red.Relation{ 177 | name: :like, 178 | direction: :out, 179 | entity: %Red.Entity{class: :user, id: 42} 180 | }, 181 | meta: %Red.Query.Meta{ 182 | limit: 80, 183 | offset: 0 184 | } 185 | } 186 | """ 187 | @spec limit(Relation.t | Query.t, pos_integer | -1) :: Query.t 188 | def limit(%Relation{} = relation, l), do: relation |> query |> limit(l) 189 | 190 | def limit(%Query{} = query, l) do 191 | %{query | meta: %{query.meta | limit: l}} 192 | end 193 | 194 | @doc ~S""" 195 | Adds a page offset to the query being mounted. 196 | 197 | ## Examples 198 | 199 | iex> {:user, 42} |> Red.relation(:like) |> Red.offset(21) 200 | %Red.Query{ 201 | queryable: %Red.Relation{ 202 | name: :like, 203 | direction: :out, 204 | entity: %Red.Entity{class: :user, id: 42} 205 | }, 206 | meta: %Red.Query.Meta{ 207 | limit: -1, 208 | offset: 21 209 | } 210 | } 211 | """ 212 | @spec offset(Relation.t | Query.t, non_neg_integer) :: Query.t 213 | def offset(%Relation{} = relation, o), do: relation |> query |> offset(o) 214 | 215 | def offset(%Query{} = query, o) do 216 | %{query | meta: %{query.meta | offset: o}} 217 | end 218 | 219 | @doc ~S""" 220 | Adds relationships in redis. 221 | 222 | ## Examples 223 | 224 | iex> likes = "user#42" |> Red.relation(:like) 225 | ...> {:ok, _} = likes |> Red.add!("user#21") 226 | ...> likes |> Enum.member?("user#21") 227 | true 228 | 229 | iex> likes = "user#21" |> Red.relation(:like) 230 | ...> {:ok, _} = likes |> Red.add!(["user#12", "user#13"]) 231 | ...> likes |> Enum.count 232 | 2 233 | """ 234 | @spec add!(Relationt.t, [Entity.conversible_to_entity] | Entity.conversible_to_entity) :: {:ok, [non_neg_integer]} 235 | def add!(%Relation{} = relation, end_entities) when is_list(end_entities) do 236 | end_entities 237 | |> Stream.map(&Red.edge relation, &1) 238 | |> Stream.map(&Edge.ops &1, :add) 239 | |> Enum.reduce(&(&2 ++ &1)) 240 | |> Client.pipeline_exec 241 | end 242 | 243 | def add!(%Red.Relation{} = relation, end_entity) do 244 | relation 245 | |> Red.edge(end_entity) 246 | |> Edge.ops(:add) 247 | |> Client.pipeline_exec 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /lib/red/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Client do 2 | @moduledoc """ 3 | A interface to talk with [redis](http://redis.io), through `Redix`. 4 | """ 5 | 6 | @type args :: [String.t | number, ...] | (String.t | number) 7 | 8 | @type redix_result :: {:ok, Redix.Protocol.redis_value} | 9 | {:error, atom | Redix.Error.t} 10 | 11 | @spec exec(args, String.t) :: redix_result 12 | def exec(args, command) when is_list(args) do 13 | redis() 14 | |> Redix.command([command] ++ args) 15 | end 16 | 17 | def exec(args, command), do: exec([args], command) 18 | 19 | @spec exec!(args, String.t) :: Redix.Protocol.redis_value 20 | def exec!(args, command) do 21 | {:ok, result} = exec(args, command) 22 | result 23 | end 24 | 25 | @spec pipeline_exec([args]) :: redix_result 26 | def pipeline_exec(ops) when is_list(ops) do 27 | redis() 28 | |> Redix.pipeline(ops) 29 | end 30 | 31 | @spec redis() :: pid 32 | def redis do 33 | {:ok, conn} = Redix.start_link 34 | conn 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/red/edge.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Edge do 2 | alias Red.{Entity, Relation} 3 | defstruct relation: nil, target: nil 4 | 5 | @type t :: %Red.Edge{relation: Relation.t, target: Entity.t} 6 | 7 | @spec ops(t, atom) :: [...] 8 | def ops(%__MODULE__{} = edge, :add) do 9 | [origin, target] = 10 | [edge.relation.entity, edge.target] 11 | |> Enum.map(&Red.key/1) 12 | |> conform_to_direction(edge) 13 | 14 | [ 15 | ["ZADD", "#{origin}:#{edge.relation.name}:in", 0, target], 16 | ["ZADD", "#{target}:#{edge.relation.name}:out", 0, origin] 17 | ] 18 | end 19 | 20 | @out %{relation: %{direction: :out}} 21 | defp conform_to_direction([origin, target], @out), do: [target, origin] 22 | defp conform_to_direction([origin, target], _in), do: [origin, target] 23 | end 24 | -------------------------------------------------------------------------------- /lib/red/entity.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Entity do 2 | defstruct id: nil, class: nil 3 | 4 | @moduledoc """ 5 | Provides the representation of an entity. 6 | 7 | The focus of Red is to persist _relations_ between entities, 8 | but not entities' data per se. 9 | 10 | Therefore, the representation of an entity is focused 11 | on its unique identification. A `%Red.Entity{id, class}` needs to have a `id`, 12 | and optionally have a `class`. 13 | 14 | The function `build/1` provides an extensive way to build entity structs. 15 | """ 16 | 17 | @type class_t :: atom | String.t 18 | @type id_t :: integer | atom | String.t 19 | @type t :: %Red.Entity{class: class_t, id: id_t} 20 | 21 | @type conversible_to_entity :: t | String.t | 22 | id_t | {class_t, id_t} | [{class_t, id_t}, ...] 23 | 24 | @doc "See `Red.entity/1` for examples" 25 | @spec build(conversible_to_entity) :: t 26 | def build(%__MODULE__{} = entity), do: entity 27 | 28 | def build(id) when is_integer(id), do: %__MODULE__{id: id} 29 | 30 | def build({class, id}), do: %__MODULE__{class: class, id: id} 31 | def build([{class, id}]), do: %__MODULE__{class: class, id: id} 32 | 33 | def build(key) when is_bitstring(key) do 34 | case key |> String.split("#") do 35 | [id] -> %__MODULE__{id: id} 36 | [class, id] -> %__MODULE__{class: class, id: id} 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/red/key.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Key do 2 | alias Red.{Entity, Relation, Edge, Query} 3 | 4 | @moduledoc ~S""" 5 | Provides utilities for building redis keys 6 | """ 7 | 8 | @doc "See `Red.key/1` for examples" 9 | @spec build(Query.queryable_t) :: String.t 10 | def build(%Relation{} = relation) do 11 | "#{build(relation.entity)}:#{relation.name}:#{relation.direction}" 12 | end 13 | 14 | def build(%Entity{class: nil} = entity), do: entity.id 15 | 16 | def build(%Entity{} = entity), do: "#{entity.class}##{entity.id}" 17 | 18 | def build(%Query{} = query), do: query.queryable |> build 19 | 20 | def build(%Edge{} = edge), do: edge.relation |> build 21 | 22 | def build({class, id}), do: %Entity{class: class, id: id} |> build 23 | 24 | def build([{class, id}]), do: {class, id} |> build 25 | 26 | def build(id) when is_number(id), do: id |> to_string |> build 27 | 28 | def build(id) when is_bitstring(id), do: id 29 | end 30 | -------------------------------------------------------------------------------- /lib/red/query.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Query do 2 | alias Red.Query.Meta 3 | alias Red.{Relation, Entity, Query, Edge} 4 | 5 | defstruct queryable: nil, meta: %Meta{} 6 | 7 | @type queryable_t :: %Relation{} | 8 | %Entity{} | 9 | %Query{} | 10 | %Edge{} | 11 | {Entity.class_t, Entity.id_t} | 12 | Entity.id_t 13 | 14 | @type t :: %Red.Query{queryable: queryable_t, meta: Meta.t} 15 | 16 | def ops(%__MODULE__{} = query, :fetch) do 17 | [ 18 | Red.key(query), 19 | query.meta.offset, 20 | limit(query) 21 | ] 22 | end 23 | 24 | defp limit(%{meta: %{limit: -1}}), do: -1 25 | 26 | defp limit(%{meta: %{limit: l}}), do: l - 1 27 | end 28 | 29 | defimpl Enumerable, for: Red.Query do 30 | alias Red.{Query, Client} 31 | 32 | def count(query) do 33 | with {:ok, count} <- 34 | query 35 | |> Red.key 36 | |> Client.exec("ZCARD"), do: {:ok, count} 37 | end 38 | 39 | def member?(query, value) do 40 | with {:ok, score} <- 41 | [query, value] 42 | |> Enum.map(&Red.key/1) 43 | |> Client.exec("ZSCORE"), do: {:ok, !is_nil(score)} 44 | end 45 | 46 | def reduce(query, acc, fun) do 47 | query 48 | |> Query.ops(:fetch) 49 | |> Client.exec!("ZREVRANGE") 50 | |> Enumerable.reduce(acc, fun) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/red/query/meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Query.Meta do 2 | defstruct offset: 0, limit: -1 3 | 4 | @moduledoc """ 5 | The struct `%Red.Query.Meta{}` contains query parameters 6 | important for paginating results: _offset_ and _limit_ 7 | """ 8 | end 9 | -------------------------------------------------------------------------------- /lib/red/relation.ex: -------------------------------------------------------------------------------- 1 | defmodule Red.Relation do 2 | alias Red.{Entity} 3 | 4 | defstruct name: nil, direction: :out, entity: nil 5 | 6 | @type name_t :: String.t | atom 7 | @type t :: %Red.Relation{name: name_t, direction: atom, entity: Entity.t} 8 | end 9 | 10 | defimpl Enumerable, for: Red.Relation do 11 | def count(relation) do 12 | relation 13 | |> Red.query 14 | |> Enumerable.count 15 | end 16 | 17 | def member?(relation, value) do 18 | relation 19 | |> Red.query 20 | |> Enumerable.member?(value) 21 | end 22 | 23 | def reduce(relation, acc, fun) do 24 | relation 25 | |> Red.query 26 | |> Enumerable.reduce(acc, fun) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Red.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :red, 6 | version: "0.2.0", 7 | elixir: "~> 1.3", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps(), 11 | 12 | description: "Persists relations between entities in Redis", 13 | 14 | package: [maintainers: ["Victor Rodrigues"], 15 | licenses: ["Apache 2.0"], 16 | links: %{"GitHub" => "https://github.com/rodrigues/red"}, 17 | files: ~w(mix.exs README.md lib)]] 18 | end 19 | 20 | def application do 21 | [applications: [:logger, :redix]] 22 | end 23 | 24 | defp deps do 25 | [{:redix, "~> 0.6.1"}, 26 | {:ex_doc, "~> 0.16.2", only: :dev}, 27 | {:credo, "~> 0.8.4", only: :dev}, 28 | {:dialyxir, "~> 0.5.0", only: :dev}] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 3 | "credo": {:hex, :credo, "0.8.4", "4e50acac058cf6292d6066e5b0d03da5e1483702e1ccde39abba385c9f03ead4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], [], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, 6 | "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "redix": {:hex, :redix, "0.6.1", "20986b0e02f02b13e6f53c79a1ae70aa83147488c408f40275ec261f5bb0a6d0", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}} 8 | -------------------------------------------------------------------------------- /test/follow_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Follow do 2 | def followers(id) do 3 | id 4 | |> user_entity 5 | |> Red.relation(:follow, :in) 6 | end 7 | 8 | def following(id) do 9 | id 10 | |> user_entity 11 | |> Red.relation(:follow, :out) 12 | end 13 | 14 | def followers_count(id) do 15 | id 16 | |> followers 17 | |> Enum.count 18 | end 19 | 20 | def following_count(id) do 21 | id 22 | |> following 23 | |> Enum.count 24 | end 25 | 26 | def following?(following_id, follower_id) do 27 | follower_id 28 | |> followers 29 | |> Enum.member?(following_id |> user_entity) 30 | end 31 | 32 | def followed_by?(follower_id, following_id) do 33 | following_id 34 | |> following?(follower_id) 35 | end 36 | 37 | def create(following_id, follower_id) do 38 | following_id 39 | |> following 40 | |> Red.add!(follower_id |> user_entity) 41 | end 42 | 43 | defp user_entity(id), do: {:user, id} 44 | end 45 | 46 | defmodule FollowTest do 47 | use ExUnit.Case 48 | import Follow 49 | 50 | setup do 51 | Red.Client.exec([], "FLUSHDB") 52 | :ok 53 | end 54 | 55 | test "follow example" do 56 | {:ok, _} = create(1, 2) # user#1 ~> follow ~> user#2 57 | {:ok, _} = create(3, 2) # user#3 ~> follow ~> user#2 58 | 59 | followers(2) 60 | |> Red.add!({:user, 42}) 61 | 62 | assert following(1) |> Enum.to_list == ~w(user#2) 63 | assert following(2) |> Enum.to_list == [] 64 | assert followers(2) |> Enum.to_list == ~w(user#42 user#3 user#1) 65 | 66 | assert following?(1, 2) 67 | assert followed_by?(2, 1) 68 | assert following?(3, 2) 69 | 70 | assert followers_count(1) == 0 71 | assert followers_count(2) == 3 72 | assert followers_count(3) == 0 73 | 74 | assert following_count(1) == 1 75 | assert following_count(2) == 0 76 | assert following_count(3) == 1 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/red_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RedTest do 2 | use ExUnit.Case 3 | doctest Red 4 | 5 | setup do 6 | Red.Client.exec([], "FLUSHDB") 7 | :ok 8 | end 9 | 10 | def user_followers do 11 | {:user, 42} 12 | |> Red.relation(:follow) 13 | end 14 | 15 | def validate_query(query) do 16 | assert query.queryable.entity.class == :user 17 | assert query.queryable.entity.id == 42 18 | assert query.queryable.direction == :out 19 | assert query.queryable.name == :follow 20 | 21 | query 22 | end 23 | 24 | test "query(relation)" do 25 | user_followers() 26 | |> Red.query 27 | |> validate_query 28 | end 29 | 30 | test "limit, receiving relation" do 31 | query = 32 | user_followers() 33 | |> Red.limit(20) 34 | |> validate_query 35 | 36 | assert query.meta.limit == 20 37 | end 38 | 39 | test "limit, receiving a query" do 40 | query = 41 | user_followers() 42 | |> Red.limit(20) 43 | |> Red.limit(19) 44 | |> validate_query 45 | 46 | assert query.meta.limit == 19 47 | end 48 | 49 | test "offset, receiving relation" do 50 | query = 51 | user_followers() 52 | |> Red.offset(13) 53 | |> validate_query 54 | 55 | assert query.meta.offset == 13 56 | end 57 | 58 | test "offset, receiving a query" do 59 | query = 60 | user_followers() 61 | |> Red.offset(13) 62 | |> Red.offset(31) 63 | |> validate_query 64 | 65 | assert query.meta.offset == 31 66 | end 67 | 68 | test "create and fetch a relation" do 69 | user_followers() 70 | |> Red.add!({:user, 21}) 71 | 72 | followers = 73 | user_followers() 74 | |> Enum.to_list 75 | 76 | assert followers == ["user#21"] 77 | end 78 | 79 | test "fetch with limit" do 80 | user_followers() 81 | |> Red.add!([{:user, 21}, "user#30", "root"]) 82 | 83 | followers = 84 | user_followers() 85 | |> Red.limit(2) 86 | |> Enum.to_list 87 | 88 | assert length(followers) == 2 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------