Skip to content

Commit 800b4e7

Browse files
authored
Merge pull request #1 from novaframework/add-developer-tools
Add developer tools section
2 parents 6b97009 + a2f95a7 commit 800b4e7

File tree

15 files changed

+681
-5
lines changed

15 files changed

+681
-5
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"id": { "type": "integer", "description": "Unique identifier" },
6+
"name": { "type": "string", "description": "Product name" },
7+
"price": { "type": "number", "description": "Price in cents" },
8+
"description": { "type": "string", "description": "Product description" }
9+
},
10+
"required": ["name", "price"]
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"id": { "type": "integer", "description": "Unique identifier" },
6+
"name": { "type": "string", "description": "User's full name" },
7+
"email": { "type": "string", "format": "email", "description": "Email address" }
8+
},
9+
"required": ["name", "email"]
10+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-module(my_first_nova_products_controller).
2+
-export([
3+
list/1,
4+
show/1,
5+
create/1,
6+
update/1,
7+
delete/1
8+
]).
9+
10+
list(_Req) ->
11+
Products = [
12+
#{id => 1, name => <<"Widget">>, price => 999, description => <<"A fine widget">>},
13+
#{id => 2, name => <<"Gadget">>, price => 1999, description => <<"A fancy gadget">>}
14+
],
15+
{json, #{products => Products}}.
16+
17+
show(#{bindings := #{<<"id">> := Id}}) ->
18+
{json, #{id => binary_to_integer(Id),
19+
name => <<"Widget">>,
20+
price => 999,
21+
description => <<"A fine widget">>}};
22+
show(_Req) ->
23+
{status, 400, #{}, #{error => <<"missing id">>}}.
24+
25+
create(#{params := #{<<"name">> := Name, <<"price">> := Price}} = Req) ->
26+
Desc = maps:get(<<"description">>, maps:get(params, Req, #{}), <<>>),
27+
{json, 201, #{}, #{id => 3, name => Name, price => Price, description => Desc}};
28+
create(_Req) ->
29+
{status, 422, #{}, #{error => <<"name and price required">>}}.
30+
31+
update(#{bindings := #{<<"id">> := Id},
32+
params := #{<<"name">> := Name, <<"price">> := Price}} = Req) ->
33+
Desc = maps:get(<<"description">>, maps:get(params, Req, #{}), <<>>),
34+
{json, #{id => binary_to_integer(Id), name => Name, price => Price, description => Desc}};
35+
update(_Req) ->
36+
{status, 422, #{}, #{error => <<"name and price required">>}}.
37+
38+
delete(#{bindings := #{<<"id">> := _Id}}) ->
39+
{status, 204};
40+
delete(_Req) ->
41+
{status, 400, #{}, #{error => <<"missing id">>}}.

examples/my_first_nova/src/my_first_nova_router.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ routes(_Environment) ->
6666
{"/notes/:id", fun my_first_nova_notes_api_controller:delete/1, #{methods => [delete]}},
6767
{"/users", fun my_first_nova_api_controller:index/1, #{methods => [get]}},
6868
{"/users/:id", fun my_first_nova_api_controller:show/1, #{methods => [get]}},
69-
{"/users", fun my_first_nova_api_controller:create/1, #{methods => [post]}}
69+
{"/users", fun my_first_nova_api_controller:create/1, #{methods => [post]}},
70+
{"/products", fun my_first_nova_products_controller:list/1, #{methods => [get]}},
71+
{"/products/:id", fun my_first_nova_products_controller:show/1, #{methods => [get]}},
72+
{"/products", fun my_first_nova_products_controller:create/1, #{methods => [post]}},
73+
{"/products/:id", fun my_first_nova_products_controller:update/1, #{methods => [put]}},
74+
{"/products/:id", fun my_first_nova_products_controller:delete/1, #{methods => [delete]}}
7075
]
7176
}
7277
].
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-module(my_first_nova_products_controller_tests).
2+
-include_lib("eunit/include/eunit.hrl").
3+
4+
list_returns_products_test() ->
5+
Req = #{},
6+
Result = my_first_nova_products_controller:list(Req),
7+
?assertMatch({json, #{products := [_|_]}}, Result).
8+
9+
show_existing_product_test() ->
10+
Req = #{bindings => #{<<"id">> => <<"1">>}},
11+
Result = my_first_nova_products_controller:show(Req),
12+
?assertMatch({json, #{id := 1, name := _, price := _}}, Result).
13+
14+
create_with_valid_params_test() ->
15+
Req = #{params => #{<<"name">> => <<"Widget">>, <<"price">> => 999}},
16+
Result = my_first_nova_products_controller:create(Req),
17+
?assertMatch({json, 201, #{}, #{id := 3, name := <<"Widget">>, price := 999}}, Result).
18+
19+
create_missing_params_test() ->
20+
Req = #{},
21+
Result = my_first_nova_products_controller:create(Req),
22+
?assertMatch({status, 422, _, _}, Result).
23+
24+
update_product_test() ->
25+
Req = #{bindings => #{<<"id">> => <<"1">>},
26+
params => #{<<"name">> => <<"Updated">>, <<"price">> => 1500}},
27+
Result = my_first_nova_products_controller:update(Req),
28+
?assertMatch({json, #{id := 1, name := <<"Updated">>, price := 1500}}, Result).
29+
30+
delete_product_test() ->
31+
Req = #{bindings => #{<<"id">> => <<"1">>}},
32+
Result = my_first_nova_products_controller:delete(Req),
33+
?assertMatch({status, 204}, Result).

src/SUMMARY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
- [Sub-Applications](building-app/sub-applications.md)
3131
- [Deployment](building-app/deployment.md)
3232

33+
# Developer Tools
34+
35+
- [Code Generators](developer-tools/code-generators.md)
36+
- [OpenAPI & API Documentation](developer-tools/openapi.md)
37+
- [Inspection & Audit Tools](developer-tools/inspection-tools.md)
38+
3339
# Going Further
3440

3541
- [Pub/Sub](going-further/pubsub.md)

src/appendix/cheat-sheet.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,29 @@ nova_pubsub:get_local_members(Channel)
199199
| `rebar3 as prod release` | Build production release |
200200
| `rebar3 as prod tar` | Build release tarball |
201201
| `rebar3 dialyzer` | Run type checker |
202+
203+
## Nova developer tool commands
204+
205+
| Command | Description |
206+
|---|---|
207+
| `rebar3 nova gen_controller --name NAME` | Generate a controller with stub actions |
208+
| `rebar3 nova gen_resource --name NAME` | Generate controller + JSON schema + route hints |
209+
| `rebar3 nova gen_test --name NAME` | Generate a Common Test suite |
210+
| `rebar3 nova openapi` | Generate OpenAPI 3.0.3 spec + Swagger UI |
211+
| `rebar3 nova config` | Show Nova configuration with defaults |
212+
| `rebar3 nova middleware` | Show global and per-group plugin chains |
213+
| `rebar3 nova audit` | Find routes missing security callbacks |
214+
| `rebar3 nova release` | Build release with auto-generated OpenAPI |
215+
216+
### Generator options
217+
218+
```shell
219+
# Controller with specific actions
220+
rebar3 nova gen_controller --name products --actions list,show,create
221+
222+
# OpenAPI with custom output
223+
rebar3 nova openapi --output priv/assets/openapi.json --title "My API" --api-version 1.0.0
224+
225+
# Release with specific profile
226+
rebar3 nova release --profile staging
227+
```

src/building-apis/json-apis.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ So far we have been rendering HTML views with ErlyDTL templates. Now let's build
66

77
Instead of returning `{ok, Variables}` (which renders a template), return `{json, Data}` and Nova encodes it as JSON with the correct `Content-Type` header.
88

9-
Create `src/controllers/my_first_nova_api_controller.erl`:
9+
You can scaffold an API controller quickly with the code generator:
10+
11+
```shell
12+
rebar3 nova gen_resource --name users --actions index,show,create
13+
```
14+
15+
This generates a controller with stub functions, a JSON schema, and prints route definitions. See [Code Generators](../developer-tools/code-generators.md) for the full details.
16+
17+
Let's write the controller by hand so we can see what each part does. Create `src/controllers/my_first_nova_api_controller.erl`:
1018

1119
```erlang
1220
-module(my_first_nova_api_controller).

src/building-app/crud-app.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,17 @@ row_to_map({Id, Title, Body, Author, InsertedAt}) ->
9696

9797
## JSON API controller
9898

99-
Create `src/controllers/my_first_nova_notes_api_controller.erl`:
99+
We can scaffold the API controller and a JSON schema in one step with the code generator:
100+
101+
```shell
102+
rebar3 nova gen_resource --name notes
103+
===> Writing src/controllers/my_first_nova_notes_api_controller.erl
104+
===> Writing priv/schemas/note.json
105+
```
106+
107+
This gives us a controller with stub functions and a JSON schema that the [OpenAPI generator](../developer-tools/openapi.md) can pick up later. Now let's replace the stubs with our actual implementation.
108+
109+
Create (or replace) `src/controllers/my_first_nova_notes_api_controller.erl`:
100110

101111
```erlang
102112
-module(my_first_nova_notes_api_controller).

src/building-app/deployment.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ Build a production release:
115115
rebar3 as prod release
116116
```
117117

118+
If you have JSON schemas in `priv/schemas/`, you can use `nova release` instead. It automatically regenerates the OpenAPI spec before building:
119+
120+
```shell
121+
rebar3 nova release
122+
===> Generated priv/assets/openapi.json
123+
===> Generated priv/assets/swagger.html
124+
===> Release successfully assembled: _build/prod/rel/my_first_nova
125+
```
126+
127+
This ensures your deployed application always ships with up-to-date API documentation. See [OpenAPI & API Documentation](../developer-tools/openapi.md) for details.
128+
118129
Start it:
119130

120131
```shell

0 commit comments

Comments
 (0)