├── .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 | [![Build Status](https://travis-ci.org/andreapavoni/plug_ets_cache.svg?branch=master)](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 | --------------------------------------------------------------------------------