feat: basic livebook generator and mix task (#420)

This commit is contained in:
Josh Price 2022-10-18 00:52:24 +11:00 committed by GitHub
parent ce35d41ed1
commit 3f572c65a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 267 additions and 5 deletions

View file

@ -0,0 +1,114 @@
defmodule Ash.Api.Info.Livebook do
@moduledoc """
Generate a Livebook from a specified API.
"""
# Strip Elixir off front of module
defp module_name(module) do
module
|> Module.split()
|> Enum.join(".")
end
# TODO: move to Ash.Resource.Info as it's also used in diagram
defp resource_name(resource) do
resource
|> Ash.Resource.Info.short_name()
|> to_string()
|> Macro.camelize()
end
defp short_module(module) do
module
|> Module.split()
|> List.last()
end
defp class_short_type({:array, t}), do: "#{short_module(t)}[]"
defp class_short_type(t), do: short_module(t)
def overview(apis) do
"""
#{for api <- apis, do: api_section(api)}
"""
end
def api_section(api) do
"""
# API #{module_name(api)}
## Class Diagram
```mermaid
#{Ash.Api.Info.Diagram.mermaid_class_diagram(api) |> String.trim()}
```
## ER Diagram
```mermaid
#{Ash.Api.Info.Diagram.mermaid_er_diagram(api) |> String.trim()}
```
## Resources
#{for resource <- Ash.Api.Info.resources(api) do
"""
- [#{resource_name(resource)}](##{resource_name(resource) |> String.downcase()})
"""
end}
#{for resource <- Ash.Api.Info.resources(api) do
resource_section(resource)
end |> Enum.join("\n")}
"""
end
def resource_section(resource) do
"""
## #{resource_name(resource)}
#{Ash.Resource.Info.description(resource)}
### Attributes
#{attr_header() |> String.trim()}
#{for attr <- Ash.Resource.Info.attributes(resource) do
attr_section(attr)
end |> Enum.join("\n")}
### Actions
#{action_header() |> String.trim()}
#{for action <- Ash.Resource.Info.actions(resource) do
action_section(action)
end |> Enum.join("\n")}
"""
end
def attr_header do
"""
| Name | Type | Description |
| ---- | ---- | ----------- |
"""
end
def attr_section(attr) do
"| **#{attr.name}** | #{class_short_type(attr.type)} | #{attr.description} |"
end
def action_header do
"""
| Name | Type | Description | Args |
| ---- | ---- | ----------- | ---- |
"""
end
def action_section(action) do
"| **#{action.name}** | _#{action.type}_ | #{action.description} | <ul>#{action_arg_section(action)}</ul> |"
end
def action_arg_section(action) do
for arg <- action.arguments do
"<li><b>#{arg.name}</b> <i>#{class_short_type(arg.type)}</i> #{arg.description}</li>"
end
end
end

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.Ash.GenerateLivebook do
@moduledoc """
Generates a Livebook for each Ash API.
## Command line options
* `--only` - only generates the given API file
"""
use Mix.Task
@shortdoc "Generates a Livebook for each Ash API"
def run(_argv) do
Mix.Task.run("compile")
File.write!("livebook.livemd", Ash.Api.Info.Livebook.overview(apis()))
Mix.shell().info("Generated Livebook")
end
def apis do
Mix.Project.config()[:app]
|> Application.get_env(:ash_apis, [])
end
end

115
test/api/livebook_test.exs Normal file
View file

@ -0,0 +1,115 @@
defmodule Ash.Test.Api.Info.LivebookTest do
@moduledoc false
use ExUnit.Case, async: true
test "generate a livebook API section from a given API" do
assert Ash.Api.Info.Livebook.api_section(Ash.Test.Flow.Api) ==
"""
# API Ash.Test.Flow.Api
## Class Diagram
```mermaid
classDiagram
class User {
UUID id
String first_name
String last_name
String email
Org org
destroy(UUID id, String first_name, String last_name, String email)
read()
for_org(UUID org)
create(UUID org, UUID id, String first_name, String last_name, ...)
update(UUID id, String first_name, String last_name, String email)
approve()
unapprove()
}
class Org {
UUID id
String name
User[] users
destroy(UUID id, String name)
update(UUID id, String name)
read()
create(UUID id, String name)
by_name(String name)
}
Org -- User
```
## ER Diagram
```mermaid
erDiagram
User {
UUID id
String first_name
String last_name
String email
}
Org {
UUID id
String name
}
Org ||--|| User : ""
```
## Resources
- [User](#user)
- [Org](#org)
## User
User model
### Attributes
| Name | Type | Description |
| ---- | ---- | ----------- |
| **id** | UUID | PK |
| **first_name** | String | User's first name |
| **last_name** | String | User's last name |
| **email** | String | User's email address |
| **approved** | Boolean | Is the user approved? |
| **org_id** | UUID | |
### Actions
| Name | Type | Description | Args |
| ---- | ---- | ----------- | ---- |
| **destroy** | _destroy_ | | <ul></ul> |
| **read** | _read_ | | <ul></ul> |
| **for_org** | _read_ | | <ul><li><b>org</b> <i>UUID</i> </li></ul> |
| **create** | _create_ | | <ul><li><b>org</b> <i>UUID</i> </li></ul> |
| **update** | _update_ | | <ul></ul> |
| **approve** | _update_ | | <ul></ul> |
| **unapprove** | _update_ | | <ul></ul> |
## Org
Org model
### Attributes
| Name | Type | Description |
| ---- | ---- | ----------- |
| **id** | UUID | |
| **name** | String | |
### Actions
| Name | Type | Description | Args |
| ---- | ---- | ----------- | ---- |
| **destroy** | _destroy_ | | <ul></ul> |
| **update** | _update_ | | <ul></ul> |
| **read** | _read_ | | <ul></ul> |
| **create** | _create_ | | <ul></ul> |
| **by_name** | _read_ | | <ul><li><b>name</b> <i>String</i> </li></ul> |
"""
end
end

View file

@ -2,6 +2,10 @@ defmodule Ash.Test.Flow.Org do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
resource do
description "Org model"
end
identities do
identity :unique_name, [:name], pre_check_with: Ash.Test.Flow.Api
end

View file

@ -2,6 +2,10 @@ defmodule Ash.Test.Flow.User do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
resource do
description "User model"
end
actions do
defaults [:read, :destroy]
@ -32,13 +36,13 @@ defmodule Ash.Test.Flow.User do
end
attributes do
uuid_primary_key :id
attribute :first_name, :string
attribute :last_name, :string
attribute :email, :string
uuid_primary_key :id, description: "PK"
attribute :first_name, :string, description: "User's first name"
attribute :last_name, :string, description: "User's last name"
attribute :email, :string, description: "User's email address"
attribute :approved, :boolean do
description "Is the user approved?"
private? true
end
end