├── test ├── test_helper.exs └── elixiak_test.exs ├── .gitignore ├── lib ├── elixiak.ex └── elixiak │ ├── util.ex │ ├── model.ex │ └── obj.ex ├── NOTICE ├── mix.exs ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mix.lock 2 | _build 3 | deps 4 | ebin -------------------------------------------------------------------------------- /lib/elixiak.ex: -------------------------------------------------------------------------------- 1 | defmodule Elixiak do 2 | 3 | end -------------------------------------------------------------------------------- /lib/elixiak/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Elixiak.Util do 2 | def list_to_args([], accum) do 3 | accum 4 | end 5 | 6 | def list_to_args([{key, val}|rest], accum) do 7 | list_to_args(rest, [{binary_to_atom(key), val}| accum]) 8 | end 9 | end -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Elixiak 2 | Copyright 2012-2013 Drew Kerrigan 3 | 4 | This software contains code derived from the Elixir Lang Ecto 5 | domain specific language library. 6 | 7 | The original software is available from 8 | https://github.com/elixir-lang/ecto -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Elixiak.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :elixiak, 6 | version: "0.0.1", 7 | elixir: "~> 0.11.0", 8 | deps: deps ] 9 | end 10 | 11 | # Configuration for the OTP application 12 | def application do 13 | [] 14 | end 15 | 16 | defp deps do 17 | [{ :'riak-elixir-client', github: "drewkerrigan/riak-elixir-client" }, 18 | { :json, github: "cblage/elixir-json"}] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | [ "json": {:git, "git://github.com/cblage/elixir-json.git", "9b55d6439ce61d7712a1218766c5cfffe011d070", []}, 2 | "meck": {:git, "git://github.com/basho/meck.git", "2b25a30a8688f94106d07f23a9f1fb523ac00f08", [{:tag, "0.8.1"}]}, 3 | "protobuffs": {:git, "git://github.com/basho/erlang_protobuffs.git", "37f9b1c50abd867302b9ff5c3ca59ea157cdf3e5", [{:tag, "0.8.1p3"}]}, 4 | "riak-elixir-client": {:git, "git://github.com/drewkerrigan/riak-elixir-client.git", "e219c2968a78fbff7a3651076bb871b654c82cdc", []}, 5 | "riak_pb": {:git, "git://github.com/basho/riak_pb", "318f043c98657c951661de751bfc0bdf7e53524d", [{:tag, "2.0.0.10"}]}, 6 | "riakc": {:git, "git://github.com/basho/riak-erlang-client.git", "8f2cc9e53f899f6f4d939e7d91a65d5e6ba3b289", []} ] 7 | -------------------------------------------------------------------------------- /test/elixiak_test.exs: -------------------------------------------------------------------------------- 1 | defmodule User do 2 | use Elixiak.Model 3 | 4 | document "user" do 5 | field :first_name, :string, indexed: true 6 | field :last_name, :string, indexed: true 7 | field :age, :integer, default: 18, indexed: true 8 | end 9 | end 10 | 11 | defmodule ElixiakTest do 12 | use ExUnit.Case 13 | 14 | def delete_all([]) do 15 | :ok 16 | end 17 | def delete_all([key|rest]) do 18 | Riak.delete "user", key 19 | delete_all(rest) 20 | end 21 | def delete_all(bucket) do 22 | {:ok, keys} = Riak.Bucket.keys(bucket) 23 | delete_all(keys) 24 | end 25 | 26 | setup do 27 | #Abstract into an Elixiak Database module? 28 | Riak.start 29 | Riak.configure(host: '127.0.0.1', port: 8087) 30 | 31 | delete_all("user") 32 | :ok 33 | end 34 | 35 | test "ping" do 36 | assert(Riak.ping == :pong) 37 | end 38 | 39 | test "secondary indexes" do 40 | u1 = User.create(first_name: "Drew", last_name: "Kerrigan", age: 10).save! 41 | u2 = User.create(first_name: "Drew", last_name: "Kerrigan", age: 20).save! 42 | u3 = User.create(first_name: "Drew", last_name: "Kerrigan", age: 30).save! 43 | u4 = User.create(first_name: "Drew", last_name: "Kerrigan", age: 40).save! 44 | 45 | drew_results = User.find(first_name: "Drew") 46 | assert(is_list(drew_results)) 47 | assert(List.last(drew_results).last_name == "Kerrigan") 48 | 49 | age_results = User.find(age: [20, 40]) 50 | assert(is_list(age_results)) 51 | assert(List.last(age_results).first_name == "Drew") 52 | 53 | u1.delete! 54 | u2.delete! 55 | u3.delete! 56 | u4.delete! 57 | end 58 | 59 | test "crud operations" do 60 | 61 | u = User.create(key: "drew", first_name: "Drew2", last_name: "Kerrigan", age: 200) 62 | .save! 63 | 64 | assert(u.last_name == "Kerrigan") 65 | assert(User.find("drew").first_name == "Drew2") 66 | 67 | u = User.find("drew") 68 | 69 | u.first_name("Harry").save! 70 | 71 | assert(User.find("drew").first_name == "Harry") 72 | 73 | u = User.find("drew") 74 | 75 | u.delete! 76 | 77 | assert(User.find("drew") == nil) 78 | 79 | User.create(key: "drew2", first_name: "Drew2", last_name: "Kerrigan", age: 200) 80 | .save! 81 | 82 | u = User.find("drew2") 83 | 84 | assert(User.find("drew2").first_name == "Drew2") 85 | 86 | User.delete "drew2" 87 | 88 | assert(User.find("drew2") == nil) 89 | end 90 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixiak 2 | 3 | An OO-dispatch style active-record-like wrapper for riak-elixir-client. If you prefer pure functional style usage, please use riak-elixir-client. 4 | 5 | ###Setup 6 | 7 | Add this project as a depency in your mix.exs 8 | 9 | ``` 10 | defp deps do 11 | [{ :elixiak, github: "drewkerrigan/elixiak" }] 12 | end 13 | ``` 14 | 15 | Install dependencies 16 | 17 | ``` 18 | mix deps.get 19 | ``` 20 | 21 | Compile 22 | 23 | ``` 24 | mix 25 | ``` 26 | 27 | ###Connect to Riak 28 | 29 | ``` 30 | Riak.start 31 | Riak.configure(host: '127.0.0.1', port: 10017) 32 | ``` 33 | 34 | ###Create a model with an embedded document 35 | 36 | This functionality is inspired by and derived from [Ecto](https://github.com/elixir-lang/ecto) by [Elixir Lang](http://elixir-lang.org/). For more information about the embedded document specifics, it is currently derived from Ecto's queryable macro and entity module. 37 | 38 | Specifying the "indexed: true" will automatically add that field and it's value as a secondary index in Riak 39 | 40 | ``` 41 | defmodule User do 42 | use Elixiak.Model 43 | 44 | document "user" do 45 | field :first_name, :string, indexed: true 46 | field :last_name, :string 47 | field :age, :integer, default: 18, indexed: true 48 | end 49 | end 50 | ``` 51 | 52 | ###Save a value 53 | 54 | With a key 55 | 56 | ``` 57 | User.create(key: "drew", first_name: "Drew", last_name: "Kerrigan", age: 200).save! 58 | ``` 59 | 60 | Without a key 61 | 62 | ``` 63 | user = User.create(first_name: "Drew", last_name: "Kerrigan", age: 200).save! 64 | user.key 65 | ``` 66 | 67 | From JSON 68 | 69 | ``` 70 | User.from_json(json_string).save! 71 | ``` 72 | 73 | ###Find an object 74 | 75 | ``` 76 | User.find(key) 77 | ``` 78 | 79 | ###Using Secondary Index (Equality Query) 80 | 81 | ``` 82 | User.find(first_name: "Drew") 83 | ``` 84 | ###Using Secondary Index (Range Query) 85 | 86 | ``` 87 | User.find(age: [20, 40]) 88 | ``` 89 | 90 | ###Delete an object 91 | 92 | Using key 93 | 94 | ``` 95 | User.delete key 96 | ``` 97 | 98 | Or object 99 | 100 | ``` 101 | user.delete! 102 | ``` 103 | 104 | ###Run tests 105 | 106 | ``` 107 | mix test 108 | ``` 109 | 110 | ### License 111 | 112 | Copyright 2012-2013 Drew Kerrigan. 113 | 114 | Licensed under the Apache License, Version 2.0 (the "License"); 115 | you may not use this file except in compliance with the License. 116 | You may obtain a copy of the License at 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | 120 | Unless required by applicable law or agreed to in writing, software 121 | distributed under the License is distributed on an "AS IS" BASIS, 122 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 123 | See the License for the specific language governing permissions and 124 | limitations under the License. -------------------------------------------------------------------------------- /lib/elixiak/model.ex: -------------------------------------------------------------------------------- 1 | defmodule Elixiak.Model do 2 | defmacro __using__(_opts) do 3 | quote do 4 | import Elixiak.Model.Document 5 | alias Elixiak.Util 6 | use Riak.Object 7 | 8 | def from_keys([], results) do results end 9 | def from_keys([key|t], results) do 10 | from_keys(t, [find(key)|results]) 11 | end 12 | 13 | def find([{field, [st, en]}]) do 14 | {keys, _terms, _continuation} = Riak.Index.query(bucket, __MODULE__.Obj.__obj__(:indexes)[field], st, en, []) 15 | from_keys(keys, []) 16 | end 17 | 18 | def find([{field, value}]) do 19 | {keys, _terms, _continuation} = Riak.Index.query(bucket, __MODULE__.Obj.__obj__(:indexes)[field], value, []) 20 | from_keys(keys, []) 21 | end 22 | 23 | def find(key) do 24 | from_robj(Riak.find bucket, key) 25 | end 26 | 27 | def delete(key) do 28 | Riak.delete bucket, key 29 | end 30 | 31 | # Utility functions 32 | def bucket() do 33 | __MODULE__.__model__(:name) 34 | end 35 | 36 | def from_json(json) do 37 | {:ok, decoded} = JSON.decode(json) 38 | __MODULE__.create(Util.list_to_args(HashDict.to_list(decoded), [])) 39 | end 40 | 41 | def add_indexes([], o) do 42 | o 43 | end 44 | 45 | def add_indexes([{name, ind}|t], o) do 46 | add_indexes(t, RObj.put_index(o, ind, [o.model.Obj.__obj__(:obj_kw, o)[name]])) 47 | end 48 | 49 | def add_indexes(o) do 50 | add_indexes(o.model.Obj.__obj__(:indexes), o) 51 | end 52 | 53 | def from_robj(nil) do 54 | nil 55 | end 56 | def from_robj(robj) when is_list(robj) do 57 | robj 58 | end 59 | def from_robj(robj) do 60 | {:ok, decoded} = JSON.decode(robj.data) 61 | 62 | add_indexes(__MODULE__.new(Util.list_to_args(HashDict.to_list(decoded), [])) 63 | .key(robj.key) 64 | .metadata(robj.metadata) 65 | .vclock(robj.vclock) 66 | .bucket(robj.bucket) 67 | .content_type(robj.content_type)) 68 | end 69 | 70 | def create() do 71 | __MODULE__.new() 72 | end 73 | def create(args) do 74 | o = __MODULE__.new(args).bucket(bucket) 75 | from_robj(RObj.from_robj(o.to_robj())) 76 | end 77 | end 78 | end 79 | end 80 | 81 | defmodule Elixiak.Model.Document do 82 | 83 | defmacro document(name, { :__aliases__, _, _ } = obj) do 84 | quote bind_quoted: [name: name, obj: obj] do 85 | def new(), do: unquote(obj).new() 86 | def new(params), do: unquote(obj).new(params) 87 | def __model__(:name), do: unquote(name) 88 | def __model__(:obj), do: unquote(obj) 89 | end 90 | end 91 | 92 | defmacro document(name, opts // [], [do: block]) do 93 | quote do 94 | name = unquote(name) 95 | opts = unquote(opts) 96 | 97 | defmodule Obj do 98 | use Elixiak.Obj, Keyword.put(opts, :model, unquote(__CALLER__.module)) 99 | unquote(block) 100 | end 101 | 102 | document(name, Obj) 103 | end 104 | end 105 | end -------------------------------------------------------------------------------- /lib/elixiak/obj.ex: -------------------------------------------------------------------------------- 1 | defmodule Elixiak.Obj do 2 | @type t :: Record.t 3 | 4 | defmacro field(name, type, opts // []) do 5 | quote do 6 | Elixiak.Obj.__field__(__MODULE__, unquote(name), unquote(type), unquote(opts)) 7 | end 8 | end 9 | 10 | @types %w(boolean string integer float binary list datetime interval virtual)a 11 | 12 | defmacro __using__(opts) do 13 | quote bind_quoted: [opts: opts] do 14 | import Elixiak.Obj 15 | 16 | def save!(o) do 17 | Riak.put o 18 | end 19 | 20 | def delete!(o) do 21 | Riak.delete o.bucket, o.key 22 | end 23 | 24 | def to_robj(o) do 25 | unless o.key do o = o.key(:undefined) end 26 | 27 | {:ok, json} = JSON.encode(o.__obj__(:obj_kw, o)) 28 | 29 | robj = :riakc_obj.new( 30 | o.bucket, 31 | o.key, 32 | json, 33 | o.content_type) 34 | 35 | if o.vclock do robj = :riakc_obj.set_vclock(robj, o.vclock) end 36 | if o.metadata do robj = :riakc_obj.update_metadata(robj, o.metadata) end 37 | 38 | robj 39 | end 40 | 41 | @before_compile Elixiak.Obj 42 | @elixiak_fields [] 43 | @record_fields [] 44 | 45 | @elixiak_model opts[:model] 46 | field(:model, :virtual, default: opts[:model]) 47 | field(:key, :virtual, default: nil) 48 | field(:bucket, :virtual, default: nil) 49 | field(:metadata, :virtual, default: nil) 50 | field(:vclock, :virtual, default: nil) 51 | field(:content_type, :virtual, default: "application/json") 52 | field(:indexes, :virtual, default: []) 53 | end 54 | end 55 | 56 | @doc false 57 | defmacro __before_compile__(env) do 58 | mod = env.module 59 | 60 | all_fields = Module.get_attribute(mod, :elixiak_fields) |> Enum.reverse 61 | record_fields = Module.get_attribute(mod, :record_fields) 62 | Record.deffunctions(record_fields, env) 63 | 64 | fields = Enum.filter(all_fields, fn({ _, opts }) -> 65 | opts[:type] != :virtual 66 | end) 67 | 68 | [ elixiak_fields(fields), 69 | elixiak_helpers(fields, all_fields)] 70 | end 71 | 72 | def __field__(mod, name, type, opts) do 73 | check_type!(type) 74 | fields = Module.get_attribute(mod, :elixiak_fields) 75 | 76 | clash = Enum.any?(fields, fn({ prev, _ }) -> name == prev end) 77 | if clash do 78 | raise ArgumentError, message: "field `#{name}` was already set on obj" 79 | end 80 | 81 | record_fields = Module.get_attribute(mod, :record_fields) 82 | 83 | Module.put_attribute(mod, :record_fields, record_fields ++ [{ name, opts[:default] }]) 84 | Module.put_attribute(mod, :elixiak_fields, [{ name, [type: type] ++ opts }|fields]) 85 | end 86 | 87 | ## Helpers 88 | 89 | defp check_type!({ outer, inner }) when is_atom(outer) do 90 | check_type!(outer) 91 | check_type!(inner) 92 | end 93 | 94 | defp check_type!(type) do 95 | unless type in @types do 96 | raise ArgumentError, message: "`#{Macro.to_string(type)}` is not a valid field type" 97 | end 98 | end 99 | 100 | defp elixiak_fields(fields) do 101 | 102 | quoted = Enum.map(fields, fn({ name, opts }) -> 103 | quote do 104 | def __obj__(:field, unquote(name)), do: unquote(opts) 105 | def __obj__(:field_type, unquote(name)), do: unquote(opts[:type]) 106 | end 107 | end) 108 | 109 | field_names = Enum.map(fields, &elem(&1, 0)) 110 | 111 | indexes = Enum.filter_map(fields, 112 | fn({ _name, opts }) -> opts[:indexed] == true end, 113 | fn({ name, opts }) -> 114 | if opts[:type] == :integer do 115 | {name, {:integer_index, "#{name}"}} 116 | else 117 | {name, {:binary_index, "#{name}"}} 118 | end 119 | end) 120 | 121 | quoted ++ [ quote do 122 | def __obj__(:field, _), do: nil 123 | def __obj__(:field_type, _), do: nil 124 | def __obj__(:field_names), do: unquote(field_names) 125 | def __obj__(:indexes), do: unquote(indexes) 126 | end ] 127 | end 128 | 129 | defp elixiak_helpers(fields, all_fields) do 130 | field_names = Enum.map(fields, &elem(&1, 0)) 131 | all_field_names = Enum.map(all_fields, &elem(&1, 0)) 132 | 133 | quote do 134 | # TODO: This can be optimized 135 | def __obj__(:allocate, values) do 136 | zip = Enum.zip(unquote(field_names), values) 137 | __MODULE__.new(zip) 138 | end 139 | 140 | def __obj__(:obj_kw, obj, opts // []) do 141 | [_module|values] = tuple_to_list(obj) 142 | zipped = Enum.zip(unquote(all_field_names), values) 143 | 144 | Enum.filter(zipped, fn { field, _ } -> 145 | __obj__(:field, field) 146 | end) 147 | end 148 | end 149 | end 150 | end --------------------------------------------------------------------------------