├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── docker-compose.yml ├── lib ├── rest_api.ex └── rest_api │ ├── endpoint.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── priv └── repo │ ├── migrations │ └── 20151213225826_create_post.exs │ └── seeds.exs ├── test ├── controllers │ └── post_controller_test.exs ├── models │ └── post_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ └── layout_view_test.exs └── web ├── channels └── user_socket.ex ├── controllers └── post_controller.ex ├── models └── post.ex ├── router.ex ├── views ├── changeset_view.ex ├── error_view.ex ├── layout_view.ex └── post_view.ex └── web.ex /.dockerignore: -------------------------------------------------------------------------------- 1 | # .dockerignore 2 | .git 3 | Dockerfile 4 | 5 | # Mix artifacts 6 | _build 7 | deps 8 | *.ez 9 | 10 | # Generate on crash by the VM 11 | erl_crash.dump 12 | 13 | # Static artifacts 14 | node_modules 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # The config/prod.secret.exs file by default contains sensitive 11 | # data and you should not commit it into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets file as long as you replace its contents by environment 15 | # variables. 16 | /config/prod.secret.exs -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM voidlock/elixir:1.1 2 | 3 | # install psql 4 | RUN apt-get update && apt-get install -y postgresql-client 5 | 6 | # install mix and rebar 7 | RUN mix local.hex --force && \ 8 | mix local.rebar --force 9 | 10 | # configure work directory 11 | RUN mkdir -p /usr/src/app 12 | WORKDIR /usr/src/app 13 | 14 | # install dependencies 15 | COPY mix.* /usr/src/app/ 16 | COPY config /usr/src/app/ 17 | RUN mix do deps.get, deps.compile 18 | 19 | CMD ["mix", "phoenix.server"] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Phoenix Rest API 2 | 3 | This project is all generated by phoenix framework. 4 | Actually I just spent time removing the static files and configuring the application to run with docker. 5 | Which is a good point to the framework because at least to the bootstrap makes everything faster and easier. 6 | 7 | Cloning this project: 8 | 9 | ```bash 10 | git clone git@github.com:maxcnunes/elixir-phoenix-rest-api.git rest_api 11 | ``` 12 | 13 | ## Development 14 | 15 | ### Running Locally 16 | 17 | To start your Phoenix app: 18 | 19 | 1. Install dependencies with `mix deps.get` 20 | 1. Set the env variable `DATABASE_URL="ecto://user:password@db/rest_api_dev"` (you may change the values) 21 | 1. Create and migrate your database with `mix ecto.create && mix ecto.migrate` 22 | 1. Start Phoenix endpoint with `mix phoenix.server` 23 | 24 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 25 | 26 | ### Running With Docker 27 | 28 | 1. Create and migrate your database with `docker-compose run --rm local sh -c "mix ecto.create && mix ecto.migrate` 29 | 1. Start Phoenix endpoint with `docker-compose run --rm local` 30 | 31 | Now you can visit `:` from your browser. 32 | Or in case your are using [dockito-proxy](https://github.com/dockito/proxy) [api.local.dockito.org](api.local.dockito.org). 33 | 34 | 35 | ### Example requests with CURL 36 | 37 | **List** 38 | ```bash 39 | curl http://api.local.dockito.org/posts 40 | ``` 41 | 42 | **Create** 43 | ```bash 44 | curl -H "Content-Type: application/json" \ 45 | -X POST \ 46 | -d '{"post": {"title":"Title 001","content":"Content 001"} }' \ 47 | http://api.local.dockito.org/posts 48 | ``` 49 | 50 | **Update** 51 | ```bash 52 | curl -H "Content-Type: application/json" \ 53 | -X PUT \ 54 | -d '{"post": {"title":"Title 001 Updated","content":"Content 001 Updated"} }' \ 55 | http://api.local.dockito.org/posts/1 56 | ``` 57 | 58 | **Delete** 59 | ```bash 60 | curl -X DELETE \ 61 | http://api.local.dockito.org/posts/1 62 | ``` 63 | 64 | ## Test 65 | 66 | ### Testing Locally 67 | 68 | ```shell 69 | mix test 70 | ``` 71 | 72 | ### Testing With Docker 73 | 74 | ```shell 75 | docker-compose run --rm test 76 | ``` 77 | -------------------------------------------------------------------------------- /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 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :rest_api, RestApi.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "+22YU3S+0MSV9BeIwSuqFIBdVNIAidsbWFSqVFyoyTE7LkwzjeCj1+6LEv8yKyuX", 13 | render_errors: [accepts: ~w(html json)], 14 | pubsub: [name: RestApi.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | # Import environment specific config. This must remain at the bottom 23 | # of this file so it overrides the configuration defined above. 24 | import_config "#{Mix.env}.exs" 25 | 26 | # Configure phoenix generators 27 | config :phoenix, :generators, 28 | migration: true, 29 | binary_id: false 30 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :rest_api, RestApi.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | cache_static_lookup: false, 14 | check_origin: false, 15 | watchers: [] 16 | 17 | # Do not include metadata nor timestamps in development logs 18 | config :logger, :console, format: "[$level] $message\n" 19 | 20 | # Set a higher stacktrace during development. 21 | # Do not configure such in production as keeping 22 | # and calculating stacktraces is usually expensive. 23 | config :phoenix, :stacktrace_depth, 20 24 | 25 | # Configure your database 26 | config :rest_api, RestApi.Repo, 27 | adapter: Ecto.Adapters.Postgres, 28 | url: {:system, "DATABASE_URL"}, 29 | pool_size: 10 30 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :rest_api, RestApi.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :rest_api, RestApi.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :rest_api, RestApi.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :rest_api, RestApi.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :rest_api, RestApi.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :rest_api, RestApi.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | url: {:system, "DATABASE_URL"}, 16 | pool: Ecto.Adapters.SQL.Sandbox 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | db: &DB 2 | image: postgres:9.4.5 3 | ports: 4 | - 5432 5 | environment: 6 | POSTGRES_USER: user 7 | POSTGRES_PASSWORD: password 8 | 9 | 10 | # Separate database for testing (since it gets deleted) 11 | dbTest: 12 | <<: *DB 13 | 14 | 15 | local: 16 | build: . 17 | command: sh -c "mix phoenix.server" 18 | ports: 19 | - 4000 20 | environment: &ENV 21 | VIRTUAL_HOST: api.local.dockito.org 22 | VIRTUAL_PORT: 4000 23 | DATABASE_URL: ecto://user:password@db/rest_api_dev 24 | volumes: 25 | - ./:/usr/src/app 26 | links: 27 | - db:db 28 | 29 | 30 | test: 31 | build: . 32 | command: sh -c "mix test" 33 | environment: 34 | DATABASE_URL: ecto://user:password@db/rest_api_test 35 | volumes: 36 | - ./:/usr/src/app 37 | links: 38 | - dbTest:db 39 | -------------------------------------------------------------------------------- /lib/rest_api.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(RestApi.Endpoint, []), 12 | # Start the Ecto repository 13 | worker(RestApi.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(RestApi.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: RestApi.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | RestApi.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rest_api/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :rest_api 3 | 4 | plug Plug.RequestId 5 | plug Plug.Logger 6 | 7 | plug Plug.Parsers, 8 | parsers: [:urlencoded, :multipart, :json], 9 | pass: ["*/*"], 10 | json_decoder: Poison 11 | 12 | plug Plug.MethodOverride 13 | plug Plug.Head 14 | 15 | plug Plug.Session, 16 | store: :cookie, 17 | key: "_rest_api_key", 18 | signing_salt: "2aIUFWNY" 19 | 20 | plug RestApi.Router 21 | end 22 | -------------------------------------------------------------------------------- /lib/rest_api/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Repo do 2 | use Ecto.Repo, otp_app: :rest_api 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :rest_api, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {RestApi, []}, 21 | applications: [:phoenix, :phoenix_html, :cowboy, :logger, 22 | :phoenix_ecto, :postgrex]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [{:phoenix, "~> 1.0.4"}, 34 | {:phoenix_ecto, "~> 1.1"}, 35 | {:postgrex, ">= 0.0.0"}, 36 | {:phoenix_html, "~> 2.1"}, 37 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 38 | {:cowboy, "~> 1.0"}] 39 | end 40 | 41 | # Aliases are shortcut or tasks specific to the current project. 42 | # For example, to create, migrate and run the seeds file at once: 43 | # 44 | # $ mix ecto.setup 45 | # 46 | # See the documentation for `Mix` for more info on aliases. 47 | defp aliases do 48 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 49 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.0.4"}, 2 | "cowlib": {:hex, :cowlib, "1.0.2"}, 3 | "decimal": {:hex, :decimal, "1.1.0"}, 4 | "ecto": {:hex, :ecto, "1.0.7"}, 5 | "fs": {:hex, :fs, "0.9.2"}, 6 | "phoenix": {:hex, :phoenix, "1.0.4"}, 7 | "phoenix_ecto": {:hex, :phoenix_ecto, "1.2.0"}, 8 | "phoenix_html": {:hex, :phoenix_html, "2.2.0"}, 9 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.1"}, 10 | "plug": {:hex, :plug, "1.0.3"}, 11 | "poison": {:hex, :poison, "1.5.0"}, 12 | "poolboy": {:hex, :poolboy, "1.5.1"}, 13 | "postgrex": {:hex, :postgrex, "0.9.1"}, 14 | "ranch": {:hex, :ranch, "1.2.0"}} 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151213225826_create_post.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Repo.Migrations.CreatePost do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:posts) do 6 | add :title, :string 7 | add :content, :string 8 | 9 | timestamps 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # RestApi.Repo.insert!(%SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/controllers/post_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.PostControllerTest do 2 | use RestApi.ConnCase 3 | 4 | alias RestApi.Post 5 | @valid_attrs %{content: "some content", title: "some content"} 6 | @invalid_attrs %{} 7 | 8 | setup do 9 | conn = conn() |> put_req_header("accept", "application/json") 10 | {:ok, conn: conn} 11 | end 12 | 13 | test "lists all entries on index", %{conn: conn} do 14 | conn = get conn, post_path(conn, :index) 15 | assert json_response(conn, 200)["data"] == [] 16 | end 17 | 18 | test "shows chosen resource", %{conn: conn} do 19 | post = Repo.insert! %Post{} 20 | conn = get conn, post_path(conn, :show, post) 21 | assert json_response(conn, 200)["data"] == %{"id" => post.id, 22 | "title" => post.title, 23 | "content" => post.content} 24 | end 25 | 26 | test "does not show resource and instead throw error when id is nonexistent", %{conn: conn} do 27 | assert_raise Ecto.NoResultsError, fn -> 28 | get conn, post_path(conn, :show, -1) 29 | end 30 | end 31 | 32 | test "creates and renders resource when data is valid", %{conn: conn} do 33 | conn = post conn, post_path(conn, :create), post: @valid_attrs 34 | assert json_response(conn, 201)["data"]["id"] 35 | assert Repo.get_by(Post, @valid_attrs) 36 | end 37 | 38 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 39 | conn = post conn, post_path(conn, :create), post: @invalid_attrs 40 | assert json_response(conn, 422)["errors"] != %{} 41 | end 42 | 43 | test "updates and renders chosen resource when data is valid", %{conn: conn} do 44 | post = Repo.insert! %Post{} 45 | conn = put conn, post_path(conn, :update, post), post: @valid_attrs 46 | assert json_response(conn, 200)["data"]["id"] 47 | assert Repo.get_by(Post, @valid_attrs) 48 | end 49 | 50 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 51 | post = Repo.insert! %Post{} 52 | conn = put conn, post_path(conn, :update, post), post: @invalid_attrs 53 | assert json_response(conn, 422)["errors"] != %{} 54 | end 55 | 56 | test "deletes chosen resource", %{conn: conn} do 57 | post = Repo.insert! %Post{} 58 | conn = delete conn, post_path(conn, :delete, post) 59 | assert response(conn, 204) 60 | refute Repo.get(Post, post.id) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/models/post_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.PostTest do 2 | use RestApi.ModelCase 3 | 4 | alias RestApi.Post 5 | 6 | @valid_attrs %{content: "some content", title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Post.changeset(%Post{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Post.changeset(%Post{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias RestApi.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | 28 | # The default endpoint for testing 29 | @endpoint RestApi.Endpoint 30 | end 31 | end 32 | 33 | setup tags do 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.restart_test_transaction(RestApi.Repo, []) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias RestApi.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | import RestApi.Router.Helpers 28 | 29 | # The default endpoint for testing 30 | @endpoint RestApi.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(RestApi.Repo, []) 37 | end 38 | 39 | {:ok, conn: Phoenix.ConnTest.conn()} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias RestApi.Repo 20 | import Ecto.Model 21 | import Ecto.Query, only: [from: 2] 22 | import RestApi.ModelCase 23 | end 24 | end 25 | 26 | setup tags do 27 | unless tags[:async] do 28 | Ecto.Adapters.SQL.restart_test_transaction(RestApi.Repo, []) 29 | end 30 | 31 | :ok 32 | end 33 | 34 | @doc """ 35 | Helper for returning list of errors in model when passed certain data. 36 | 37 | ## Examples 38 | 39 | Given a User model that lists `:name` as a required field and validates 40 | `:password` to be safe, it would return: 41 | 42 | iex> errors_on(%User{}, %{password: "password"}) 43 | [password: "is unsafe", name: "is blank"] 44 | 45 | You could then write your assertion like: 46 | 47 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 48 | 49 | You can also create the changeset manually and retrieve the errors 50 | field directly: 51 | 52 | iex> changeset = User.changeset(%User{}, password: "password") 53 | iex> {:password, "is unsafe"} in changeset.errors 54 | true 55 | """ 56 | def errors_on(model, data) do 57 | model.__struct__.changeset(model, data).errors 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ["--quiet"] 4 | Mix.Task.run "ecto.migrate", ["--quiet"] 5 | Ecto.Adapters.SQL.begin_test_transaction(RestApi.Repo) 6 | 7 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ErrorViewTest do 2 | use RestApi.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(RestApi.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(RestApi.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(RestApi.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RestApi.LayoutViewTest do 2 | use RestApi.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "rooms:*", RestApi.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # RestApi.Endpoint.broadcast("users_socket:" <> user.id, "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /web/controllers/post_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.PostController do 2 | use RestApi.Web, :controller 3 | 4 | alias RestApi.Post 5 | 6 | plug :scrub_params, "post" when action in [:create, :update] 7 | 8 | def index(conn, _params) do 9 | posts = Repo.all(Post) 10 | render(conn, "index.json", posts: posts) 11 | end 12 | 13 | def create(conn, %{"post" => post_params}) do 14 | changeset = Post.changeset(%Post{}, post_params) 15 | 16 | case Repo.insert(changeset) do 17 | {:ok, post} -> 18 | conn 19 | |> put_status(:created) 20 | |> put_resp_header("location", post_path(conn, :show, post)) 21 | |> render("show.json", post: post) 22 | {:error, changeset} -> 23 | conn 24 | |> put_status(:unprocessable_entity) 25 | |> render(RestApi.ChangesetView, "error.json", changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | post = Repo.get!(Post, id) 31 | render(conn, "show.json", post: post) 32 | end 33 | 34 | def update(conn, %{"id" => id, "post" => post_params}) do 35 | post = Repo.get!(Post, id) 36 | changeset = Post.changeset(post, post_params) 37 | 38 | case Repo.update(changeset) do 39 | {:ok, post} -> 40 | render(conn, "show.json", post: post) 41 | {:error, changeset} -> 42 | conn 43 | |> put_status(:unprocessable_entity) 44 | |> render(RestApi.ChangesetView, "error.json", changeset: changeset) 45 | end 46 | end 47 | 48 | def delete(conn, %{"id" => id}) do 49 | post = Repo.get!(Post, id) 50 | 51 | # Here we use delete! (with a bang) because we expect 52 | # it to always work (and if it does not, it will raise). 53 | Repo.delete!(post) 54 | 55 | send_resp(conn, :no_content, "") 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /web/models/post.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Post do 2 | use RestApi.Web, :model 3 | 4 | schema "posts" do 5 | field :title, :string 6 | field :content, :string 7 | 8 | timestamps 9 | end 10 | 11 | @required_fields ~w(title content) 12 | @optional_fields ~w() 13 | 14 | @doc """ 15 | Creates a changeset based on the `model` and `params`. 16 | 17 | If no params are provided, an invalid changeset is returned 18 | with no validation performed. 19 | """ 20 | def changeset(model, params \\ :empty) do 21 | model 22 | |> cast(params, @required_fields, @optional_fields) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Router do 2 | use RestApi.Web, :router 3 | 4 | pipeline :api do 5 | plug :accepts, ["json"] 6 | end 7 | 8 | scope "/", RestApi do 9 | pipe_through :api 10 | 11 | resources "/posts", PostController 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ChangesetView do 2 | use RestApi.Web, :view 3 | 4 | def render("error.json", %{changeset: changeset}) do 5 | # When encoded, the changeset returns its errors 6 | # as a JSON object. So we just pass it forward. 7 | %{errors: changeset} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.ErrorView do 2 | use RestApi.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.LayoutView do 2 | use RestApi.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/post_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.PostView do 2 | use RestApi.Web, :view 3 | 4 | def render("index.json", %{posts: posts}) do 5 | %{data: render_many(posts, RestApi.PostView, "post.json")} 6 | end 7 | 8 | def render("show.json", %{post: post}) do 9 | %{data: render_one(post, RestApi.PostView, "post.json")} 10 | end 11 | 12 | def render("post.json", %{post: post}) do 13 | %{id: post.id, 14 | title: post.title, 15 | content: post.content} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule RestApi.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use RestApi.Web, :controller 9 | use RestApi.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Model 22 | 23 | import Ecto.Changeset 24 | import Ecto.Query, only: [from: 1, from: 2] 25 | end 26 | end 27 | 28 | def controller do 29 | quote do 30 | use Phoenix.Controller 31 | 32 | alias RestApi.Repo 33 | import Ecto.Model 34 | import Ecto.Query, only: [from: 1, from: 2] 35 | 36 | import RestApi.Router.Helpers 37 | end 38 | end 39 | 40 | def view do 41 | quote do 42 | use Phoenix.View, root: "web/templates" 43 | 44 | # Import convenience functions from controllers 45 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 46 | 47 | # Use all HTML functionality (forms, tags, etc) 48 | use Phoenix.HTML 49 | 50 | import RestApi.Router.Helpers 51 | end 52 | end 53 | 54 | def router do 55 | quote do 56 | use Phoenix.Router 57 | end 58 | end 59 | 60 | def channel do 61 | quote do 62 | use Phoenix.Channel 63 | 64 | alias RestApi.Repo 65 | import Ecto.Model 66 | import Ecto.Query, only: [from: 1, from: 2] 67 | end 68 | end 69 | 70 | @doc """ 71 | When used, dispatch to the appropriate controller/view/etc. 72 | """ 73 | defmacro __using__(which) when is_atom(which) do 74 | apply(__MODULE__, which, []) 75 | end 76 | end 77 | --------------------------------------------------------------------------------