├── .formatter.exs
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config
└── config.exs
├── lib
├── plug_ets_cache.ex
└── plug_ets_cache
│ ├── phoenix.ex
│ ├── plug.ex
│ ├── response.ex
│ └── store.ex
├── mix.exs
├── mix.lock
└── test
├── phoenix_test.exs
├── plug_test.exs
├── response_test.exs
├── store_test.exs
├── support
└── index.txt.eex
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | inputs: [
3 | "lib/**/*.{ex,exs}",
4 | "test/**/*.{ex,exs}",
5 | "mix.exs"
6 | ]
7 | ]
8 |
--------------------------------------------------------------------------------
/.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 | .elixir_ls
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.5
4 | - 1.6
5 | - 1.7
6 | otp_release:
7 | - 18.3
8 | - 19.3
9 | - 20.3
10 | - 21.0
11 | matrix:
12 | exclude:
13 | - elixir: 1.5
14 | otp_release: 21.0
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Andrea Pavoni
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PlugEtsCache
2 |
3 | [](https://travis-ci.org/andreapavoni/plug_ets_cache)
4 |
5 | A simple http response caching system based on [Plug](https://github.com/elixir-lang/plug) and [ETS](http://erlang.org/doc/man/ets.html). It easily integrates in every application that uses Plug, including a Phoenix dedicated adapter.
6 |
7 | The main use case is when the contents of your web pages don't change in real time and are served to a multitude of visitors. Even if your server response times are in order of few tens of milliseconds, caching pages into ETS (hence into RAM) would shrink times to microseconds.
8 |
9 | Cache duration can be configured with a combination of a `ttl` value and `ttl_check`. Check [con_cache](https://github.com/sasa1977/con_cache) documentation for more details on this, PlugEtsCache uses it to read/write to ETS.
10 |
11 | ## Installation
12 |
13 | The package is available in [Hex](https://hex.pm/packages/plug_ets_cache), follow these steps to install:
14 |
15 | 1. Add `plug_ets_cache` to your list of dependencies in `mix.exs`:
16 |
17 | ```elixir
18 | def deps do
19 | # Get from hex
20 | [{:plug_ets_cache, "~> 0.3.0"}]
21 | # Or use the latest from master
22 | [{:plug_ets_cache, github: "andreapavoni/plug_ets_cache"}]
23 | end
24 | ```
25 |
26 | 2. Ensure `plug_ets_cache` is started before your application:
27 |
28 | ```elixir
29 | def application do
30 | [applications: [:plug_ets_cache]]
31 | end
32 | ```
33 |
34 | ## Usage
35 |
36 | These are the common steps to setup `PlugEtsCache`:
37 |
38 | 1. Set configuration in `config/config.exs` (the following values are defaults):
39 |
40 | ```elixir
41 | config :plug_ets_cache,
42 | db_name: :ets_cache,
43 | ttl_check: 60,
44 | ttl: 300
45 | ```
46 |
47 | 2. Add `PlugEtsCache.Plug` to your router/plug:
48 |
49 | ```elixir
50 | plug PlugEtsCache.Plug
51 | ```
52 |
53 | Now follow specific instructions below for your use case.
54 |
55 | ### With Phoenix
56 |
57 | Because Phoenix has a more complex lifecycle when it comes to send a response, it
58 | has a special module for this.
59 |
60 | 1. Add ` use PlugEtsCache.Phoenix`
61 | 2. Call `cache_response` *after you've sent a response*:
62 |
63 | ```elixir
64 | defmodule MyApp.SomeController do
65 | use MyApp.Web, :controller
66 | use PlugEtsCache.Phoenix
67 |
68 | # ...
69 |
70 | def index(conn, _params) do
71 | # ...
72 | conn
73 | |> render("index.html")
74 | |> cache_response
75 | end
76 |
77 | # ...
78 | end
79 | ```
80 |
81 | ### With plain Plug
82 |
83 | Supposing a very simple Plug module:
84 |
85 | 1. Import `PlugEtsCache.Response.cache_response/1` inside your module
86 | 2. Call `cache_response` *after you've sent a response*:
87 |
88 | ```elixir
89 | defmodule FooController do
90 | use Plug.Router
91 | import PlugEtsCache.Response, only: [cache_response: 1]
92 |
93 | plug :match
94 | plug :dispatch
95 |
96 | get "/" do
97 | Plug.Conn.fetch_query_params(conn)
98 | |> put_resp_content_type("text/plain")
99 | |> send_resp(200, "Hello cache")
100 | |> cache_response
101 | end
102 | end
103 | ```
104 |
105 | ### Setting TTL
106 |
107 | `cache_response/1` will adhere to the `ttl` value in the config, but
108 | you can instead use `cache_response/2` to specify a custom `ttl` for each
109 | response. Examples:
110 |
111 | ```elixir
112 | cache_response(conn, :timer.hours(1))
113 | ```
114 |
115 | ```elixir
116 | cache_response(conn, ttl: :timer.minutes(45))
117 | ```
118 |
119 | ### Using a custom cache key
120 |
121 | If you need greater control over the key used to cache the request you can use a custom function to build the cache key.
122 | The function needs to accept one argument, the `Plug.Conn` struct, and return the key.
123 |
124 | ```elixir
125 | cache_response(conn, [cache_key: fn conn -> conn.request_path end, ttl: :timer.minutes(10)])
126 | ```
127 |
128 | ## Documentation
129 |
130 | The docs can be found at [https://hexdocs.pm/plug_ets_cache](https://hexdocs.pm/plug_ets_cache).
131 |
132 | ## TODO
133 |
134 | * add more detailed docs
135 |
136 | ## Contributing
137 |
138 | Everyone is welcome to contribute to PlugEtsCache and help tackling existing issues!
139 |
140 | Use the [issue tracker](https://github.com/andreapavoni/plug_ets_cache/issues) for bug reports or feature requests.
141 |
142 | Please, do your best to follow the [Elixir's Code of Conduct](https://github.com/elixir-lang/elixir/blob/master/CODE_OF_CONDUCT.md).
143 |
144 | ## License
145 |
146 | This source code is released under MIT License. Check [LICENSE](https://github.com/andreapavoni/plug_ets_cache/blob/master/LICENSE) file for more information.
147 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | config :plug_ets_cache,
6 | db_name: :ets_cache,
7 | ttl_check: 60,
8 | ttl: 300
9 |
--------------------------------------------------------------------------------
/lib/plug_ets_cache.ex:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache do
2 | @moduledoc """
3 | Implements an ETS based cache storage for Plug based applications.
4 | """
5 |
6 | use Application
7 |
8 | def start(_type, _args) do
9 | import Supervisor.Spec, warn: false
10 |
11 | children = [
12 | worker(ConCache, [
13 | [
14 | name: app_env(:db_name, :ets_cache),
15 | ttl_check_interval: :timer.seconds(app_env(:ttl_check, 60)),
16 | global_ttl: :timer.seconds(app_env(:ttl, 300))
17 | ]
18 | ])
19 | ]
20 |
21 | opts = [strategy: :one_for_one, name: PlugEtsCache]
22 | Supervisor.start_link(children, opts)
23 | end
24 |
25 | defp app_env(key, default) do
26 | Application.get_env(:plug_ets_cache, key, default)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/plug_ets_cache/phoenix.ex:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Phoenix.Controller) do
2 | defmodule PlugEtsCache.Phoenix do
3 | defmacro __using__(_) do
4 | quote do
5 | import Phoenix.View, only: [render_to_string: 3]
6 |
7 | def cache_response(conn) do
8 | cache_response(conn, [])
9 | end
10 |
11 | def cache_response(conn, ttl) when is_number(ttl) or is_atom(ttl) do
12 | cache_response(conn, [ttl: ttl])
13 | end
14 |
15 | def cache_response(conn, opts) when is_list(opts) do
16 | content_type =
17 | conn
18 | |> Plug.Conn.get_resp_header("content-type")
19 | |> hd
20 |
21 | view = view_module(conn)
22 | template = view_template(conn)
23 |
24 | layout =
25 | case layout(conn) do
26 | false ->
27 | false
28 |
29 | {layout_module, layout_template} ->
30 | format = layout_formats(conn) |> hd
31 | {layout_module, "#{layout_template}.#{format}"}
32 | end
33 |
34 | assigns = Map.merge(conn.assigns, %{conn: conn, layout: layout})
35 | response = render_to_string(view, template, assigns)
36 |
37 | PlugEtsCache.Store.set(conn, content_type, response, opts)
38 | conn
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/plug_ets_cache/plug.ex:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.Plug do
2 | @moduledoc """
3 | A Plug used to send cached responses if any.
4 |
5 | Example usage:
6 | defmodule MyApp.Router do
7 | # use/import modules depending on your framework/lib
8 | plug PlugEtsCache.Plug
9 | end
10 | """
11 |
12 | alias PlugEtsCache.Store
13 | import Plug.Conn
14 |
15 |
16 | def init(opts) when is_list(opts) do
17 | opts
18 | end
19 |
20 | def init(_), do: []
21 |
22 | def call(conn, opts) do
23 | case Store.get(conn, opts) do
24 | nil ->
25 | conn
26 |
27 | result ->
28 | conn
29 | |> put_resp_content_type(result.type, nil)
30 | |> send_resp(200, result.value)
31 | |> halt
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/plug_ets_cache/response.ex:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.Response do
2 | @moduledoc """
3 | This module contains the helper to cache a Plug response.
4 |
5 | Example usage:
6 | defmodule MyApp.SomeController do
7 | # use/import modules depending on your framework/lib
8 |
9 | import PlugEtsCache.Response, only: [cache_response: 1]
10 |
11 | def index(conn, _params) do
12 | render(conn, "index.txt", %{value: "cache"})
13 | |> cache_response
14 | end
15 | end
16 | """
17 |
18 | def cache_response(%Plug.Conn{state: :unset} = conn), do: conn
19 |
20 | def cache_response(%Plug.Conn{state: :sent} = conn), do: cache_response(conn, nil)
21 |
22 | def cache_response(%Plug.Conn{state: :unset} = conn, _ttl), do: conn
23 |
24 | def cache_response(%Plug.Conn{state: :sent, resp_body: body, status: status} = conn, ttl) when status in 200..299 do
25 | content_type =
26 | conn
27 | |> Plug.Conn.get_resp_header("content-type")
28 | |> hd
29 |
30 | PlugEtsCache.Store.set(conn, content_type, body, ttl)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/plug_ets_cache/store.ex:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.Store do
2 | @moduledoc """
3 | This module contains functions to get/set cached responses.
4 |
5 | Example usage:
6 | conn = %Plug.Conn{request_path: "/test", query_string: ""}
7 |
8 | PlugEtsCache.Store.set(conn, "text/plain", "Hello cache")
9 |
10 | PlugEtsCache.Store.get(conn) # %{type: "text/plain", value: "Hello cache"}
11 |
12 | It's usually not needed to use these functions directly because the response
13 | caching happens in `PlugEtsCache.Response` and `PlugEtsCache.Plug`.
14 | """
15 |
16 | @doc """
17 | Retrieves eventual cached content based on `conn` params.
18 | """
19 | def get(%Plug.Conn{} = conn, opts \\ []) do
20 | cache_key_fn = Keyword.get(opts, :cache_key, &key/1)
21 | ConCache.get(db_name(), cache_key_fn.(conn))
22 | end
23 |
24 | @doc """
25 | Stores `conn` response data in the cache.
26 | """
27 | def set(%Plug.Conn{} = conn, type, value) when is_binary(value) do
28 | set(conn, type, value, [])
29 | end
30 |
31 | @doc """
32 | Stores `conn` response data in the cache with specified ttl.
33 | """
34 | def set(%Plug.Conn{} = conn, type, value, ttl) when is_binary(value) and (is_number(ttl) or is_atom(ttl)) do
35 | set(conn, type, value, ttl: ttl)
36 | end
37 |
38 | @doc """
39 | Stores `conn` response data in the cache with the specified options.
40 |
41 | Recognized options:
42 |
43 | - `ttl`, the ttl of the item (i.e `[ttl: 1_000]`)
44 | - `cache_key`, a function taking one argument (the conn) that returns the key for to use for the item in the cache.
45 | """
46 | def set(%Plug.Conn{} = conn, type, value, opts) when is_list(opts) do
47 | ttl = Keyword.get(opts, :ttl)
48 | cache_key_fn = Keyword.get(opts, :cache_key, &key/1)
49 |
50 | item = case ttl do
51 | nil -> %{type: type, value: value}
52 | _else -> %ConCache.Item{value: %{type: type, value: value}, ttl: ttl}
53 | end
54 |
55 | ConCache.put(db_name(), cache_key_fn.(conn), item)
56 | conn
57 | end
58 |
59 | defp key(conn) do
60 | "#{conn.request_path}#{conn.query_string}"
61 | end
62 |
63 | defp db_name do
64 | Application.get_env(:plug_ets_cache, :db_name, :ets_cache)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :plug_ets_cache,
7 | version: "0.3.1",
8 | elixir: "~> 1.5",
9 | build_embedded: Mix.env() == :prod,
10 | start_permanent: Mix.env() == :prod,
11 | deps: deps(),
12 | package: package(),
13 | description: description(),
14 | # Docs
15 | name: "PlugEtsCache",
16 | source_url: "https://github.com/andreapavoni/plug_ets_cache",
17 | homepage_url: "https://github.com/andreapavoni/plug_ets_cache",
18 | docs: [
19 | main: "PlugEtsCache",
20 | extras: ["README.md"]
21 | ]
22 | ]
23 | end
24 |
25 | def application do
26 | [extra_applications: [:logger], mod: {PlugEtsCache, []}]
27 | end
28 |
29 | defp deps do
30 | [
31 | {:plug, "~> 1.3"},
32 | {:con_cache, "~> 0.13.0"},
33 | {:phoenix, "~> 1.2 or ~> 1.3", optional: true},
34 | {:ex_doc, "~> 0.18.0", only: :dev, runtime: false}
35 | ]
36 | end
37 |
38 | defp package do
39 | [
40 | name: :plug_ets_cache,
41 | files: ["lib", "mix.exs", "README.md", "LICENSE"],
42 | maintainers: ["Andrea Pavoni"],
43 | licenses: ["MIT"],
44 | links: %{"GitHub" => "https://github.com/andreapavoni/plug_ets_cache"}
45 | ]
46 | end
47 |
48 | defp description do
49 | """
50 | A simple caching system based on Plug and ETS.
51 | """
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "con_cache": {:hex, :con_cache, "0.13.0", "fe30eeffc776465b596a60d5bc7f81083fd90aeeeb5d72b98a957fb4a78e4378", [:mix], [], "hexpm"},
3 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
4 | "ex_doc": {:hex, :ex_doc, "0.18.4", "4406b8891cecf1352f49975c6d554e62e4341ceb41b9338949077b0d4a97b949", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
5 | "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
6 | "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
7 | "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
8 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
9 | "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"},
10 | "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
11 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
12 | "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
13 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
14 | }
15 |
--------------------------------------------------------------------------------
/test/phoenix_test.exs:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Phoenix.Controller) do
2 | defmodule FakeController do
3 | use Phoenix.Controller
4 | use PlugEtsCache.Phoenix
5 |
6 | plug(:put_layout, false)
7 |
8 | def index(conn, _params) do
9 | render(conn, "index.txt", %{value: "cache"})
10 | |> cache_response
11 | end
12 |
13 | def index_with_ttl(conn, _params) do
14 | render(conn, "index.txt", %{value: "cache"})
15 | |> cache_response(:timer.seconds(30))
16 | end
17 |
18 | def index_with_opts(conn, _params) do
19 | render(conn, "index.txt", %{value: "cache_opts"})
20 | |> cache_response([ttl: :timer.seconds(30), cache_key: fn conn -> conn.request_path end])
21 | end
22 | end
23 |
24 | defmodule(FakeView, do: use(Phoenix.View, root: "test/support"))
25 |
26 | defmodule PlugEtsCache.PhoenixTest do
27 | use ExUnit.Case, async: true
28 | use Plug.Test
29 |
30 | def action(controller, verb, action, headers \\ []) do
31 | conn = conn(verb, "/", headers) |> Plug.Conn.fetch_query_params()
32 | controller.call(conn, controller.init(action))
33 | end
34 |
35 | test "caches the controller response" do
36 | conn = action(FakeController, :get, :index, "content-type": "text/plain")
37 | cached_resp = PlugEtsCache.Store.get(conn)
38 |
39 | assert conn.resp_body == "Hello cache\n"
40 | assert cached_resp.value == conn.resp_body
41 | assert cached_resp.type == "text/plain; charset=utf-8"
42 | end
43 |
44 | test "caches the controller response with ttl" do
45 | conn = action(FakeController, :get, :index_with_ttl, "content-type": "text/plain")
46 | cached_resp = PlugEtsCache.Store.get(conn)
47 |
48 | assert conn.resp_body == "Hello cache\n"
49 | assert cached_resp.value == conn.resp_body
50 | assert cached_resp.type == "text/plain; charset=utf-8"
51 | end
52 |
53 | test "caches the controller response with specified options" do
54 | conn = action(FakeController, :get, :index_with_opts, "content-type": "text/plain")
55 | cached_resp = PlugEtsCache.Store.get(conn, [cache_key: fn conn -> conn.request_path end])
56 |
57 | assert conn.resp_body == "Hello cache_opts\n"
58 | assert cached_resp.value == conn.resp_body
59 | assert cached_resp.type == "text/plain; charset=utf-8"
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/test/plug_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.PlugTest do
2 | use ExUnit.Case, async: true
3 | use Plug.Test
4 |
5 | test "replies with cached content if present" do
6 | request = %Plug.Conn{request_path: "/test", query_string: ""}
7 | PlugEtsCache.Store.set(request, "text/plain; charset=utf-8", "Hello cache")
8 |
9 | conn =
10 | conn(:get, "/test")
11 | |> PlugEtsCache.Plug.call(PlugEtsCache.Plug.init(nil))
12 |
13 | assert conn.resp_body == "Hello cache"
14 | assert {"content-type", "text/plain; charset=utf-8"} in conn.resp_headers
15 | assert conn.state == :sent
16 | assert conn.status == 200
17 | end
18 |
19 | test "does not double set charset in response headers" do
20 | request = %Plug.Conn{request_path: "/test.xml", query_string: ""}
21 | PlugEtsCache.Store.set(request, "application/xml; charset=utf-8", "hello")
22 |
23 | conn =
24 | conn(:get, "/test.xml")
25 | |> PlugEtsCache.Plug.call(PlugEtsCache.Plug.init(nil))
26 |
27 | assert conn.resp_body == "hello"
28 | assert {"content-type", "application/xml; charset=utf-8"} in conn.resp_headers
29 | assert conn.state == :sent
30 | assert conn.status == 200
31 | end
32 |
33 | test "return conn if not present" do
34 | conn =
35 | conn(:get, "/test2")
36 | |> PlugEtsCache.Plug.call(PlugEtsCache.Plug.init(nil))
37 |
38 | assert conn.state == :unset
39 | assert conn.status == nil
40 | assert conn.resp_body == nil
41 | refute {"content-type", "text/plain; charset=utf-8"} in conn.resp_headers
42 | end
43 |
44 | test "accept cache key function in options" do
45 | opts = [cache_key: &cache_key/1]
46 | request = %Plug.Conn{request_path: "/optiontest", query_string: "foo=bar"}
47 | PlugEtsCache.Store.set(request, "text/plain; charset=utf-8", "cached response", opts)
48 |
49 | conn = :get
50 | |> conn("/optiontest?foo=bar")
51 | |> PlugEtsCache.Plug.call(PlugEtsCache.Plug.init(opts))
52 |
53 | assert conn.resp_body == "cached response"
54 | assert {"content-type", "text/plain; charset=utf-8"} in conn.resp_headers
55 | assert conn.state == :sent
56 | assert conn.status == 200
57 | end
58 |
59 | defp cache_key(conn) do
60 | conn.query_string
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/test/response_test.exs:
--------------------------------------------------------------------------------
1 | defmodule FooController do
2 | use Plug.Router
3 | import PlugEtsCache.Response, only: [cache_response: 1, cache_response: 2]
4 |
5 | plug(:match)
6 | plug(:dispatch)
7 |
8 | get "/" do
9 | Plug.Conn.fetch_query_params(conn)
10 | |> put_resp_content_type("text/plain")
11 | |> send_resp(200, "Hello cache")
12 | |> cache_response
13 | end
14 |
15 | get "/ttl" do
16 | Plug.Conn.fetch_query_params(conn)
17 | |> put_resp_content_type("text/plain")
18 | |> send_resp(200, "Hello ttl cache")
19 | |> cache_response(:infinity)
20 | end
21 | end
22 |
23 | defmodule PlugEtsCache.ResponseTest do
24 | use ExUnit.Case, async: true
25 | use Plug.Test
26 |
27 | test "caches the controller response" do
28 | conn =
29 | conn(:get, "/", "content-type": "text/plain")
30 | |> Plug.Conn.fetch_query_params()
31 | |> FooController.call(FooController)
32 |
33 | cached_resp = PlugEtsCache.Store.get(conn)
34 |
35 | assert conn.resp_body == "Hello cache"
36 | assert cached_resp.value == conn.resp_body
37 | assert cached_resp.type == "text/plain; charset=utf-8"
38 | end
39 |
40 | test "caches the controller response with ttl" do
41 | conn =
42 | conn(:get, "/ttl", "content-type": "text/plain")
43 | |> Plug.Conn.fetch_query_params()
44 | |> FooController.call(FooController)
45 |
46 | cached_resp = PlugEtsCache.Store.get(conn)
47 |
48 | assert conn.resp_body == "Hello ttl cache"
49 | assert cached_resp.value == conn.resp_body
50 | assert cached_resp.type == "text/plain; charset=utf-8"
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/store_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PlugEtsCache.StoreTest do
2 | use ExUnit.Case, async: true
3 |
4 | test "sets and gets values from cache" do
5 | conn = %Plug.Conn{request_path: "/test", query_string: ""}
6 |
7 | PlugEtsCache.Store.set(conn, "text/plain", "Hello cache")
8 | cache = PlugEtsCache.Store.get(conn)
9 |
10 | assert cache.value == "Hello cache"
11 | assert cache.type == "text/plain"
12 | end
13 |
14 | test "sets and gets values from cache with ttl provided" do
15 | conn = %Plug.Conn{request_path: "/test", query_string: ""}
16 |
17 | PlugEtsCache.Store.set(conn, "text/plain", "Hello cache", 1000)
18 | cache = PlugEtsCache.Store.get(conn)
19 |
20 | assert cache.value == "Hello cache"
21 | assert cache.type == "text/plain"
22 | end
23 |
24 | test "sets and gets values from cache with custom key function" do
25 | conn = %Plug.Conn{request_path: "/test", query_string: "?foo=bar"}
26 | cache_key_fn = fn _conn -> "my_custom_key" end
27 | PlugEtsCache.Store.set(conn, "text/plain", "Hello cache with custom key", [ttl: 1000, cache_key: cache_key_fn])
28 | cache = PlugEtsCache.Store.get(conn, [cache_key: cache_key_fn])
29 |
30 | assert cache.value == "Hello cache with custom key"
31 | assert cache.type == "text/plain"
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/support/index.txt.eex:
--------------------------------------------------------------------------------
1 | Hello <%= @value %>
2 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | {:ok, _} = Application.ensure_all_started(:phoenix)
2 | ExUnit.start()
3 |
--------------------------------------------------------------------------------