zoedsoupe

zoedsoupe

Generate typespec and defstruct from runtime values?

I’m not very used to macros/compile time manipulation, but I will explain what I want to achieve and would like to know if it’s possible by any way.

I’m building a CLI framework for Elixir, with high inspirations on clap lib for rust. There you can do something like:

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Adds files to myapp
    Add { name: Option<String> },
}

fn main() {
    let cli = Cli::parse();

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    match &cli.command {
        Commands::Add { name } => {
            println!("'myapp add' was used, name is: {name:?}")
        }
    }
}

In other words, define a CLI struct and then parse the data structure with available/parsed input. So in the Elixir world I made some macros like: defcommand/2, defcommand/3 and defoption/2, that can be used as:

defmodule CLI do
 use Nexus

  defcommand :foo,
    required: true,
    type: :string,
    doc: "Command that receives a string as argument and prints it."

  defcommand :fizzbuzz, type: {:enum, ~w(fizz buzz)a}, doc: "Fizz bUZZ", required: true

  defcommand :foo_bar, type: :null, doc: "Teste" do
    defoption :some, short: :s
    defcommand :baz, default: "hello", doc: "Hello"
    defcommand :bar, default: "hello", doc: "Hello"
  end
end

So, basically, given that macros/definitions of commands I would like construct a %CLI{} struct, or better: a %__MODULE__{} struct with fields :foo, :fizzbuzz and submodules for subcommands like :foo_bar that would be a sub-struct with :baz, :bar fields and also typespecs for that, for example for the CLI module:

defmodule CLI do
  defmodule CLI.FooBar do
    @type t :: %__MODULE__{baz: String.t, bar: String.t}
    defstruct baz: “hello”, bar: “hello”
  end

  @type t :: %__MODULE__{foo: String.t, fizzbuzz: :fizz | :buzz, foo_bar: CLI.FooBar.t}
  defstruct foo: nil, fizzbuzz: nil, foo_bar: %FooBar{}
end

However, commands and flags will only be available after compilation of their respective macros, so, how would be possible to achieve I want?

The complete source code for my library (is already usable) can be found in GitHub - zoedsoupe/nexus: CLI framework for Elixir, with magic! · GitHub

EDIT 1: I think I could define a global macro called defcli, and then receive commands and flags definitions inside it, but I don’t think it matches my usage requirements :confused:.

defmodule CLI do
  use Nexus
  
  defcli do
    defoption :help, short: :h
    defcommand :foo, default: “bar”, type: :string
  end
end

That way I can build the typespec and struct with those definitions, but it would break the library contract for now, would be a super major change in the public API, so I would like another possible approach, if there is?

Marked As Solved

al2o3cr

al2o3cr

One approach that can help make macros more powerful to use and easier to write: avoid parsing the contents of do blocks if you can.

An example in Ecto.Schema:

https://github.com/elixir-ecto/ecto/blob/f2a0def734dcee354acb223116550e9ae659fb0c/lib/ecto/schema.ex#L612-L617

block is the block passed to the schema macro, which sets up a “prelude” and “postlude” and then expands the block as regular AST. This side-steps the need for code like Nexus.build_subcommands and lets users do things like:

schema do
  %w(foo bar baz wat)a
  |> Enum.each(fn name ->
    field name, :string
  end)
end

Calls to field accumulate data in module attributes, then the postlude built by schema uses them to construct __schema__ and so forth.


Another spot in that file worth reviewing for inspiration - the code in embeds_one and embeds_many that handles declaring a (nested) schema inline using a nested module:

https://github.com/elixir-ecto/ecto/blob/f2a0def734dcee354acb223116550e9ae659fb0c/lib/ecto/schema.ex#L1695-L1701


One more technique to consider: use __before_compile__ to generate code “at the end” of the module.

  • use Nexus would set up a @before_compile
  • then each call to defcommand would accumulate details in module attributes
  • then the before_compile could generate the @type and defstruct lines using all the accumulated commands

Last Post!

al2o3cr

al2o3cr

The trick is that every defcommand generates a separate module and the block gets evaluated in that module. So this code:

  defcommand :foo_bar, type: :null, doc: "Teste" do
    defoption :some, short: :s
    defcommand :baz, default: "hello", doc: "Hello"
    defcommand :bar, default: "hello", doc: "Hello"
  end

would expand to something like:

defmodule FooBar do
  use Nexus

  defoption :some, short: :s
  defcommand :baz, default: "hello", doc: "Hello"
  defcommand :bar, default: "hello", doc: "Hello"
end
stash_someplace_for_before_compile_to_use(FooBar)

The defoption and defcommands here accumulate data into module attributes on FooBar, not in the surrounding module.

Where Next?

Popular in Questions Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement