Skip to content

Commit

Permalink
documentation, comments and README
Browse files Browse the repository at this point in the history
  • Loading branch information
boonious committed Nov 14, 2019
1 parent 26c1f23 commit 4ec729a
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 25 deletions.
80 changes: 69 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,78 @@

Gerai ("store" in Malay) is an [OTP-compliant](http://blog.plataformatec.com.br/2018/04/elixir-processes-and-this-thing-called-otp/) JSON object cache for Elixir.

## Installation
## Usage

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `gerai` to your list of dependencies in `mix.exs`:
Gerai currently provides a single-process in-memory cache server for storing JSON objects.
Storage functions (CRUD) are available through both functional and HTTP interfaces.

### Example - functional interface

Start the server up through Elixir interactive shell `iex -S mix`

```elixir
def deps do
[
{:gerai, "~> 0.1.0"}
]
end
iex> Gerai.put("tt1454468", "{\"name\":\"Gravity\",\"id\":\"tt1454468\"}")
:ok

iex> Gerai.get("tt1454468")
{:ok, "{\"name\":\"Gravity\",\"id\":\"tt1454468\"}"}

iex> Gerai.put("tt1316540", "{\"name\":\"The Turin Horse\",\"id\":\"tt1316540\"}")
:ok

iex> Gerai.get(:all)
{:ok,
["{\"name\":\"The Turin Horse\",\"id\":\"tt1316540\"}",
"{\"name\":\"Gravity\",\"id\":\"tt1454468\"}"]}

iex> Gerai.delete("tt1454468")
:ok

iex> Gerai.get("tt1454468")
{:error, nil}
```
### Example - HTTP interface

The cache can also be accessed via the `http://localhost:8080/` endpoint, while the application
is running through one of the following ways:

- `iex -S mix`
- `mix run --no-halt`

```
# GET request with ID
# http://localhost:8080/?id=tt1316540
GET /?id=tt1316540
# GET all objects - request without id
# http://localhost:8080/
GET /
# Create and update objects by putting and posting data
# http://localhost:8080/tt1454468
POST /tt1454468
PUT /tt1454468
# Delete object with ID
# http://localhost:8080/tt1454468
DELETE /tt1454468
```

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/gerai](https://hexdocs.pm/gerai).
## Note

This software has been developed quickly for the experimentation of OTP.
In particular, the use of [GenServer](https://hexdocs.pm/elixir/GenServer.html) behaviour module
to enable fast implementation of an in-memory storage for JSON serialised objects.

While OTP can facilitate a very high number of concurrent processes (millions),
only a **single server process** is used in `gerai` as a global cache server with a fixed name
registration. It therefore can be accessed directly from anywhere within the OTP application. The singleton
scenario means the software is non-performant and for demo purposes only, as requests to the cache are handled
one at a time by a single server process.

Future development could involve a larger number of concurrent GenServer cache stores, for example
a pool of distributed cache servers or personalised storage.

The HTTP interface is put together using [`Plug`](https://github.com/elixir-plug/plug).

33 changes: 26 additions & 7 deletions lib/gerai.ex
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
defmodule Gerai do
@moduledoc """
Documentation for Gerai.
Client functions for caching JSON objects.
## Examples
iex> Gerai.get("tt1316540")
"{\\\"name\\\":\\\"The Turin Horse\\\",\\\"id\\\":\\\"tt1316540\\\",\\\"genre\\\":[\\\"Drama\\\"],\\\"directed_by\\\":[\\\"Béla Tarr\\\"]}"
iex> Gerai.put("tt1454468", "{\\\"name\\\":\\\"Gravity\\\",\\\"id\\\":\\\"tt1454468\\\"}")
:ok
iex> Gerai.get("tt1454468")
{:ok, "{\\\"name\\\":\\\"Gravity\\\",\\\"id\\\":\\\"tt1454468\\\"}"}
iex> Gerai.put("tt1316540", "{\\\"name\\\":\\\"The Turin Horse\\\",\\\"id\\\":\\\"tt1316540\\\"}")
:ok
iex> Gerai.get(:all)
{:ok,
["{\\\"name\\\":\\\"The Turin Horse\\\",\\\"id\\\":\\\"tt1316540\\\"}",
"{\\\"name\\\":\\\"Gravity\\\",\\\"id\\\":\\\"tt1454468\\\"}"]}
iex> Gerai.delete("tt1454468")
:ok
"""

@cache_server_name GeraiJson

@doc """
Get a serialised JSON from cache by ID
Retrieve JSON objects from cache by ID or `:all`
"""
@spec get(binary) :: {:ok, binary | list[binary]} | {:error, nil}
@spec get(binary | :all) :: {:ok, binary | list[binary]} | {:error, nil}
def get(id), do: GenServer.call(@cache_server_name, {:get, id})

# TODO: implement as asyncronuous call `cast/handle_cast`
@doc """
Cache a serialised JSON by ID
"""
# TODO: perhaps in asyncronuous mode with `cast/handle_cast`
# handles both post and put requests
@spec put(binary, binary) :: :ok | :error
def put("", _), do: :error
def put(nil, _), do: :error
def put(id, json), do: GenServer.call(@cache_server_name, {:put, id, json})

# TODO: implement as asyncronuous call `cast/handle_cast`
@doc """
Delete a JSON object from cache by ID
"""
# TODO: perhaps in asyncronuous mode with `cast/handle_cast`
@spec delete(binary) :: :ok | :error
def delete(id), do: GenServer.call(@cache_server_name, {:delete, id})
end
7 changes: 4 additions & 3 deletions lib/gerai/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ defmodule Gerai.Application do
use Application

def start(_type, _args) do
# List all child processes to be supervised
children = [
# Starts a worker by calling: Gerai.Worker.start_link(arg)
# {Gerai.Worker, arg},
# Starts the cache server, with a fixed name
# The server name could be configurable via environmental variable
{Gerai.Cache, name: GeraiJson},

# Starts the HTTP interface which could also be made optional via configuration
{Plug.Cowboy, scheme: :http, plug: Gerai.Router, options: [port: 8080]}
]

Expand Down
11 changes: 10 additions & 1 deletion lib/gerai/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,34 @@ defmodule Gerai.Cache do
@moduledoc false
use GenServer

# required to start cache server up in Supervisor tree
# start up the cache server when application is run
# via the application Supervisor tree child spec - Gerai.Application
def start_link(options \\ []) do
GenServer.start_link(__MODULE__, nil, options)
end

@impl true
def init(_args) do
state = %{}
{:ok, state}
end

# implemnting callbacks to handle CRUD features
# TODO: explore the use of asynchrounous callbacks for some requests

@impl true
def handle_call({:get, :all}, _from, state) do
{:reply, {:ok, Map.values(state)}, state}
end

@impl true
def handle_call({:get, id}, _from, state) do
object = Map.get(state, id)
reply = if object, do: {:ok, object}, else: {:error, nil}
{:reply, reply, state}
end

@impl true
def handle_call({:put, id, json_s}, _from, state) when is_binary(json_s) do
resp = Poison.decode(json_s)
status = elem(resp, 0)
Expand All @@ -33,6 +41,7 @@ defmodule Gerai.Cache do
end
end

@impl true
def handle_call({:delete, id}, _from, state) do
new_state = Map.delete(state, id)

Expand Down
5 changes: 5 additions & 0 deletions lib/gerai/router.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Simple HTTP interface that facililate and forward requests
# to service interface functions

defmodule Gerai.Router do
@moduledoc false
use Plug.Router

plug(:match)
Expand Down Expand Up @@ -47,6 +51,7 @@ defmodule Gerai.Router do
send_resp(conn, 404, "Not Found")
end

# retrieve all objects and render them as JSON array
defp get(id) when id == "" or id == nil do
{_, objects} = Gerai.get(:all)

Expand Down
23 changes: 23 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ defmodule Gerai.MixProject do
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test
],

# Docs
name: "gerai",
description: "Gerai is an OTP-compliant JSON object cache for Elixir.",
package: package(),
source_url: "https://github.com/boonious/gerai",
homepage_url: "https://github.com/boonious/gerai",
docs: [
main: "gerai",
extras: ["README.md"]
]
]
end
Expand All @@ -35,4 +46,16 @@ defmodule Gerai.MixProject do
{:poison, "~> 3.1"}
]
end

defp package do
[
name: "gerai",
maintainers: ["Boon Low"],
licenses: ["Apache 2.0"],
links: %{
Changelog: "https://github.com/boonious/gerai/blob/master/CHANGELOG.md",
GitHub: "https://github.com/boonious/gerai"
}
]
end
end
6 changes: 3 additions & 3 deletions test/gerai_test.exs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# TODO: need refactoring, cleaning and breaking up
# Could write a suite of HTTP tests with Bypass, and performance testing as well
defmodule GeraiTest do
use ExUnit.Case
use Plug.Test

# doctest Gerai

@cache_server_name GeraiJson
@opts Gerai.Router.init([])

Expand Down Expand Up @@ -41,7 +41,7 @@ defmodule GeraiTest do
end

test "put handles malformed json" do
assert Gerai.put("id","this is not a valid json string") == :error
assert Gerai.put("id", "this is not a valid json string") == :error

# json with missing id
obj = %{"directed_by" => ["Hirokazu Koreeda"]}
Expand Down

0 comments on commit 4ec729a

Please sign in to comment.