ash
ash copied to clipboard
`mix ash.new`
Getting started could be made much easier if we created a mix ash.new
similar to mix phx.new
, which would have options to easily include extensions, and add phoenix or plug. For example:
mix ash.new --json_api --graphql --policy_authorizer --csv --phoenix
I started playing with that and came with an initial sketch, currently all in one file lib/mix/tasks/ash.ex
It would be good to get some opinions on that before I continue.
defmodule Mix.Tasks.Ash.Resource do
use Mix.Task
alias Mix.Tasks.Helpers
@impl Mix.Task
@shortdoc """
Initializes new resource in lib/myapp/resources/__.ex with default content
"""
@doc """
Initializes new resource in lib/myapp/resources/__.ex with default content
run as mix ash.resource user first_name last_name password email --data_layer postgres
"""
@entry """
defmodule <%= project_name %>.<%= module_name %> do
<%= if data_layer == "postgres" do %>
use Ash.Resource, data_layer: AshPostgres.DataLayer
postgres do
repo <%= project_name %>.Repo
table "<%= name %>"
end
<% end %>
<%= if data_layer == "ets" do %>
use Ash.Resource, data_layer: Ash.DataLayer.Ets
<% end %>
attributes do
uuid_primary_key :id
<%= for attribute <- attributes do %>
attribute :<%= attribute %>, :string do
# allow_nil? false
# constraints max_length: 255
end
<% end %>
# Alternatively, you can use the keyword list syntax
# You can also set functional defaults, via passing in a zero
# argument function or an MFA
attribute :public, :boolean, allow_nil?: false, default: false
# This is set on create
create_timestamp :inserted_at
# This is updated on all updates
update_timestamp :updated_at
# `create_timestamp` above is just shorthand for:
# attribute :inserted_at, :utc_datetime_usec,
# writable?: false,
# default: &DateTime.utc_now/0
end
end
"""
def run([]), do: IO.puts("Name of resource needs to be specified")
def run(args) do
{switches, [resource | attributes], _invalid} =
OptionParser.parse(args,
aliases: [v: :verbose, d: :data_layer],
strict: [debug: :boolean, data_layer: :string]
)
data_layer = Keyword.get(switches, :data_layer, "postgres")
initial_data =
EEx.eval_string(@entry,
module_name: Helpers.capitalize(resource),
data_layer: data_layer,
project_name: Helpers.project_module_name(),
name: resource,
attributes: attributes
)
if not File.exists?(Helpers.resources_folder()), do: File.mkdir!(Helpers.resources_folder())
File.write!(Helpers.resource_file_name(resource), initial_data)
data_layer_info(data_layer)
resource_info(resource)
end
defp resource_info(resource) do
IO.puts("""
First, ensure you've added ash_postgres to your mix.exs file.
Please add your resource to #{Helpers.api_file_name("api")}
""")
end
defp data_layer_info("postgres") do
IO.puts("""
First, ensure you've added ash_postgres to your mix.exs file.
{:ash_postgres, "~> x.y.z"}
and run
mix ash_postgres.generate_migrations
""")
end
end
defmodule Mix.Tasks.Ash.New do
alias Mix.Tasks.Helpers
@entry_data """
defmodule <%= project_module_name %>.<%= module_name %> do
use Ash.Api
resources do
# add your resources here
#resource <%= project_module_name %>.ResourceName
end
end
"""
@shortdoc """
Initializes new api file lib/api.ex with default content
"""
@doc """
Initializes new api file lib/api.ex with default content
run as mix ash.new
"""
def run([]), do: run(["api"])
def run([file_name | _]) do
initial_data =
EEx.eval_string(@entry_data,
module_name: Helpers.capitalize(file_name),
project_module_name: Helpers.project_module_name()
)
File.write!(Helpers.api_file_name(file_name), initial_data)
end
end
defmodule Mix.Tasks.Helpers do
def capitalize(""), do: ""
def capitalize(name) do
with <<first::utf8, rest::binary>> <- name, do: String.upcase(<<first::utf8>>) <> rest
end
def project_module_name() do
String.split(app_name(), "_") |> Enum.map(&capitalize/1) |> Enum.join()
end
def app_name() do
Mix.Project.config()[:app] |> Atom.to_string()
end
def lib_folder(), do: "lib/" <> app_name()
def api_file_name(main_file), do: lib_folder() <> "/" <> main_file <> ".ex"
def resources_folder(), do: lib_folder() <> "/resources"
def resource_file_name(name), do: resources_folder() <> "/" <> name <> ".ex"
end
This looks great so far! Awesome work! A couple notes:
1.) I think we want them to be able to optionally specify a context, and if they do that, the file would be lib/myapp/context/resources/resource.ex
, and the module name would be App.Context.Resource
. If they specify a context for the api, that file would be lib/myapp/context/context.ex
. In my own projects I've been dropping the convention of calling the file api.ex
and the module Api
as it is mostly just superfluous.
2.) These tasks are excellent, but they would also fall more under mix ash.gen.resource
and mix ash.gen.api
. mix ash.new
should be an archive that acts as mix phx.new
does. However, we definitely need both and this is a great place to start.
Other than those things, this looks awesome and I'd be happy to get this in!
This can accept an --context param, this is not a problem. I can do this over the weekend. I need also split it to separate files, and move the templates to separate files too and then I'll push a pr
Now that I think of it though, lets call the param --api
.
And when creating an api, it can just have an optional name, such that mix ash.gen.api
makes lib/myapp/api.ex
and mix ash.gen.api context
makes lib/myapp/context/context.ex
One more thing:
I think we also want to split the extension specific parts into the actual extension repo. So what we would do is say something like:
AshPostgres.DataLayer.resource_template(...options)
That way if we update ash_postgres we can just update its template there.
ok, thanks for the suggestions
When I think now about it and we can have the postgres specific task in the ash-postgres repo so if its not installed it won't even show up in the list
this way we can have mix ash.gen.resource
and when ash-postgres is installed we would have additional mix ash.gen.resource.postgres
. The helper functions would be in the main repo so not much duplication really and you need the main repo anyway.
Also I'm thinking of having 2 different template files for it - one heavily commented and with examples and other one clean.
If you're a beginner then its nice to have examples in the same file. If you know what your'e doing then deleting every time the examples when you generate 10 resources might be a pain.
I like the idea of having a specialized template, but it might be better to have it in the same template, something like --with-guides
, that way we don't have to maintain two separate templates. I'm not 100% sure on that though, so I'm open to two separate templates.
However, I don't think we want separate tasks for ash_postgres
and ash
, because eventually someone will want to generate their resources with ash_json_api
and ash_graphql
and friends, and they would have to pick only one if we went that route. Ideally we'd see something like:
mix ash.gen.resource --json_api --graphql --postgres
And they'd get a resource with DSL for all of those extensions.
ok, no problem. I need to check how this is all done in phoenix.
Great work so far 🎉 no rush
This is more or less usable.
Feels a bit pythonic, that's my background
the largest file is generated with this command:
mix ash.gen.resource users --json-api --graphql --postgres --policy-authorizer name age integer born date --api accounts --phoenix --csv
on the other hand a minimal one is generated with:
mix ash.gen.resource users name age integer born date --no-timestamps --no-guides
Most of the logic for resource is contained in the /priv/resources.ex.eex
- the content of this file needs to be edited by someone who knows more about this project - it's my first contact with it really, and I just pasted what I found in the docs.
If someone wants to pick up from this point feel free to pick it up
I think it looks great! Just one important thing that remains on it, which I won't have time to address but anyone else can feel free to do the work: we should really split up the responsibility of the contents of the extensions specific sections. Ideally, those would live in each of the various extensions, so we don't have to release Ash when the extension changes.
@zachdaniel the template per package needs to be a separate file:
defmodule AshPostgres.Templates do
def resource_template(assigns) do
"""
postgres do
repo #{assigns.project_name}.Repo
table "#{assigns.table_name}"
end
"""
end
def guide_template(assigns) do
"""
# nothing in here - just for test
# postgres do
# repo #{assigns.project_name}.Repo
# table "#{assigns.table_name}"
# end
"""
end
end
This way I can programmatically check if it exists and if yes use it. I think we need for now have copies in ash until all the packages wont have own templates, then it can be removed Currently I use try/rescue for this and when it fails the check the local templates file I'm open to ideas
Alright, I can get behind having simple defaults for each section in core :) Let me review and we can see about getting it merged! As you mentioned, I'll probably make some updates to the templates here and there, but the structure so far looks excellent.
Ok, I'm thinking to make Ash.Template behaviour so its standarized.
Great idea 👍🏻
@dkuku could I suggest that instead of an --api flag (since I think it's required) we treat it the same as a Context in phoenix i.e.:
# instead of
mix ash.gen.resource users ... --api accounts
# we do
mix ash.gen.resource accounts users ...
This will fit the mental model of most phoenix developers. We may want to keep the start completely the same, i.e.:
# CamelCaseContext CamelCaseResourceName table_name(snake_case)
mix ash.gen.resource Accounts User users ...
Especially since you'll have a template for that in phoenix, and also we do need all of those things (if you select Postgres as the type at least)
Some additional considerations here:
-
there are different concepts of where to put the file depending on what you are doing
-
some things are sort of "contained" by others, and perhaps our
mix ash.gen
command should take that into account. For example, we could support adding a resource to the registry of an API automatically when you generate it. I'm not sure what that would ultimately look like. -
I'm going to have the latest reccomendations for how to organize your project as part of the new guides. For example, its no longer recommended that you'd call each thing an
Api
, e.gMyApp.Stuff.Api
andMyApp.Stuff.Resource
. Instead, you'd call the api module,MyApp.Stuff
(w/o theApi
at the end)
Closing this in favor of a set of features that will be introduced with the "generators" feature.