Mix task for creating component templates - #1059
Conversation
d85fa4a to
64ee4df
Compare
mat-hek
left a comment
There was a problem hiding this comment.
I have mixed feelings about generating commented-out code. I think it would be better to use comments to mark things as optional, but no strong preference
| * `inspector:` function converting fields' value to a string. Used when | ||
| creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1` | ||
| * `description:` string describing an option. It will be used for generating the docs | ||
| * `spec:` Specification of the type the values of this option can have. |
There was a problem hiding this comment.
IMO this is hard to read. Typespec is a commonly known term, easily googlable too
| * `spec:` Specification of the type the values of this option can have. | |
| * `spec:` Typespec of the values of this option |
| # -------------------------- | ||
| # --- OPTIONAL CALLBACKS --- | ||
| # -------------------------- |
There was a problem hiding this comment.
Why are these callbacks marked as optional? AFAIK callbacks are optional 🤔
There was a problem hiding this comment.
what do you mean? some, like handle_buffer, aren't
| # ----------------- | ||
| # --- CALLBACKS --- | ||
| # ----------------- |
There was a problem hiding this comment.
Maybe it would be good to link to relevant documentation or component lifecycle page?
| * `inspector:` function converting fields' value to a string. Used when | ||
| creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1` | ||
| * `description:` string describing an option. It will be used for generating the docs | ||
| * `spec:` Specification of the type the values of this option can have. |
There was a problem hiding this comment.
maybe you could add also that it will be used in t() type
There was a problem hiding this comment.
It is undone or I don't see something 😉
There was a problem hiding this comment.
I think I misunderstood, done now
| module = | ||
| case argv do | ||
| [] -> | ||
| Mix.raise(""" | ||
| Module name not provided. | ||
|
|
||
| This task expects a module name, which the created #{unquote(component_type)} will have: | ||
|
|
||
| $ mix membrane.gen.#{String.downcase(unquote(component_name))} My#{unquote(component_name)} | ||
|
|
||
| """) | ||
|
|
||
| Mix.Tasks.Help | ||
|
|
||
| [module_name | _rest] -> | ||
| Module.concat([module_name]) | ||
| end |
There was a problem hiding this comment.
Will it work if somebody types mix membrane.gen.sink -l some/path?
We could also check if the module name type is in CamelCase
There was a problem hiding this comment.
no, it won't work. Do you think it could be nice to generate a module name from path if the module name wasn't provided? I think requiring the module name is reasonable, but it's not a strong opinion.
There was a problem hiding this comment.
in theory every string can be converted to a module name with Module.concat, but when they're invalid (for example starting with lowercase letters or containing special characters) they're converted to :"Elixir.<string>" atoms, and I don't think anyone normal would want a module named like that, so I'll add validation.
| end | ||
| |> then(&Path.join(base_dir, &1)) | ||
|
|
||
| File.mkdir_p!(Path.dirname(component_path)) |
There was a problem hiding this comment.
Will it work if component_path is a/b/c/d/file.ex and b doesn't exists?
There was a problem hiding this comment.
Ok 👍 but let's use |> operator 😉
| defp infer_path_from_module(module) do | ||
| module | ||
| |> inspect() | ||
| |> String.downcase() | ||
| |> String.split(".") | ||
| |> List.update_at(-1, &"#{&1}.ex") | ||
| |> Path.join() | ||
| end |
There was a problem hiding this comment.
Will it convert AlaMaKota into alamakota.ex or ala_ma_kota.ex? I think we want the second option
|
|
||
| # def_options some_option: [ | ||
| # spec: typespec of the option, | ||
| # default: default value, |
There was a problem hiding this comment.
This applies to other components as well
| # default: default value, | |
| # default: default value (if not set, this field will be required), |
| # @impl true | ||
| # def handle_pad_added(_pad, _context, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_pad_removed(_pad, _context, state) do | ||
| # {[], state} | ||
| # end |
There was a problem hiding this comment.
Let's mention it will be executed only for dynamic pads
There was a problem hiding this comment.
I know that I mentioned dynamic pads, but now I think we should avoid this term here. If target audience of this task are beginners, it will be more obvious for them if we will talk about pads with availability: :on_request, because they can see this field in the pad definition above. Please fix it here and in similar places.
| defp infer_path_from_module(module_name) do | ||
| module_name | ||
| |> String.split(".") | ||
| |> Enum.map(&(Regex.replace(~r/(?<=[^A-Z])([A-Z])/, &1, "_\\1") |> String.downcase())) |
There was a problem hiding this comment.
Isn't it what Macro.underscore does?
There was a problem hiding this comment.
BTW, there's also Macro.camelize in case you need it
There was a problem hiding this comment.
wow I didn't know about these, thx
| # max_instances: positive integer, | ||
| # accepted_format: _any | ||
| # availability: :on_request | :always, # default - :always | ||
| # max_instances: pos_integer() | :infinity, # relevant only for dynamic pads, default - :infinity |
There was a problem hiding this comment.
Hm, IMO it would be better, because beginner may not know what dynamic pad is. Please fix here and in similar places.
| # max_instances: pos_integer() | :infinity, # relevant only for dynamic pads, default - :infinity | |
| # max_instances: pos_integer() | :infinity, # relevant only for pads with `availability: :on_request`, default - :infinity |
| # @impl true | ||
| # def handle_pad_added(_pad, _context, state) do | ||
| # {[], state} | ||
| # end | ||
|
|
||
| # @impl true | ||
| # def handle_pad_removed(_pad, _context, state) do | ||
| # {[], state} | ||
| # end |
There was a problem hiding this comment.
I know that I mentioned dynamic pads, but now I think we should avoid this term here. If target audience of this task are beginners, it will be more obvious for them if we will talk about pads with availability: :on_request, because they can see this field in the pad definition above. Please fix it here and in similar places.
|
IMO we could add some simple tests for this task with basic assertions, jut to make sure that future changes won't break it completely. E.g. we could ensure that generated templates contain all callbacks and pad definitions. |
acc2f40 to
bcd5d07
Compare
| Module.concat(Gen, unquote(component_type)).do_run(tmp, [inspect(unquote(component_type))]) | ||
|
|
||
| {{:module, module, _contents, __result}, []} = | ||
| Code.eval_file(Path.join(tmp, Macro.underscore(unquote(component_type)) <> ".ex")) |
There was a problem hiding this comment.
It could be such a beautiful |> sequence (with Kernel.<>(".ex")) 😞
| * `inspector:` function converting fields' value to a string. Used when | ||
| creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1` | ||
| * `description:` string describing an option. It will be used for generating the docs | ||
| * `spec:` Typespec of the values this option can have. |
There was a problem hiding this comment.
| * `spec:` Typespec of the values this option can have. | |
| * `spec:` Typespec of the values this option can assume. |
| # Membrane.Logger.warning(""" | ||
| # Received message but no handle_info callback has been specified. Ignoring. | ||
| # Message: #{inspect(message)}\ | ||
| # """) |
There was a problem hiding this comment.
One would need to add require Membrane.Logger for this to work after it get's uncommented
|
|
||
| ## Options | ||
| * `-l, --location` - If a target location is provided, the #{component_name} will be created there, relative to the `lib` directory. | ||
| The filename must also be present and most likely have an `.ex` extension. If location is not provided, then it will be |
There was a problem hiding this comment.
I don't like usage of "moste likely" here - is it supposed to have .ex extension or is it not? :D
| Mix.raise(""" | ||
| Module name not provided. | ||
|
|
||
| This task expects a module name, which the created #{unquote(component_type)} will have: |
There was a problem hiding this comment.
It sounds a bit odd, how about:
| This task expects a module name, which the created #{unquote(component_type)} will have: | |
| This task expects a module name, for the newly created #{unquote(component_type)}: |
| """) | ||
|
|
||
| [module_name | _rest] -> | ||
| Module.concat([module_name]) |> inspect() |
There was a problem hiding this comment.
What's the purpose of this Module.concat([module_name])? 🤔
There was a problem hiding this comment.
It validates the passed string in the context of module name, e.g. passing valid module name like "Abc" will result in unchanged "Abc" string, and passing invalid module name like "123Abc" will result in ":\"Elixir.123Abc\"", which we check later
There was a problem hiding this comment.
It works due to a kind of a "side effect" of how Module.concat/1 treats the strings (it runs String.to_atom() under the hood) 🤔 If we were to leave it this way, it definitelly requires a comment.
I did some research on how to determine that a String is a valid alias and that's what I found:
- this is how IEx searches for a valid module names within autocomplete (it resolves alias "pieces" one by one):
https://github.com/elixir-lang/elixir/blob/d33fd8e413fe98e0e54dde7ef94fadb272b1ef67/lib/iex/lib/iex/autocomplete.ex#L511 - We could also use
Code.string_to_quoted()and check if the returned value is an alias:
def proper_module_name?(name) do
case Code.string_to_quoted(name) do
{:ok, {:__aliases__, _, _}} -> true
_ -> false
end
end
There was a problem hiding this comment.
did the second thing
|
|
||
| @spec do_run(binary(), [binary()]) :: any() | ||
| def do_run(base_dir, argv) do | ||
| {path_option, argv} = OptionParser.parse!(argv, aliases: @aliases, strict: @switches) |
There was a problem hiding this comment.
Let's just call it options, as this name will get outdated if we add another option to the switches.
b84e862 to
e34bcd6
Compare
No description provided.