mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
feat: basic livebook generator and mix task (#420)
This commit is contained in:
parent
ce35d41ed1
commit
3f572c65a2
5 changed files with 267 additions and 5 deletions
114
lib/ash/api/info/livebook.ex
Normal file
114
lib/ash/api/info/livebook.ex
Normal 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
|
25
lib/mix/tasks/generate_livebook.ex
Normal file
25
lib/mix/tasks/generate_livebook.ex
Normal 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
115
test/api/livebook_test.exs
Normal 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
|
|
@ -2,6 +2,10 @@ defmodule Ash.Test.Flow.Org do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
|
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
|
||||||
|
|
||||||
|
resource do
|
||||||
|
description "Org model"
|
||||||
|
end
|
||||||
|
|
||||||
identities do
|
identities do
|
||||||
identity :unique_name, [:name], pre_check_with: Ash.Test.Flow.Api
|
identity :unique_name, [:name], pre_check_with: Ash.Test.Flow.Api
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,10 @@ defmodule Ash.Test.Flow.User do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
|
use Ash.Resource, data_layer: Ash.DataLayer.Mnesia
|
||||||
|
|
||||||
|
resource do
|
||||||
|
description "User model"
|
||||||
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :destroy]
|
||||||
|
|
||||||
|
@ -32,13 +36,13 @@ defmodule Ash.Test.Flow.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id, description: "PK"
|
||||||
attribute :first_name, :string
|
attribute :first_name, :string, description: "User's first name"
|
||||||
attribute :last_name, :string
|
attribute :last_name, :string, description: "User's last name"
|
||||||
|
attribute :email, :string, description: "User's email address"
|
||||||
attribute :email, :string
|
|
||||||
|
|
||||||
attribute :approved, :boolean do
|
attribute :approved, :boolean do
|
||||||
|
description "Is the user approved?"
|
||||||
private? true
|
private? true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue