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!

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?

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:

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:


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
2 Likes

Yeah! __before_compile__ is the way to go! Thank you so much! I really need to get more into @after_compile and @before_compile attributes!

Also the embeds_one macro technique is a good fit for my project, I’ll try to replicate to generate modules on the fly whith required env.

However, the first approach kinda struggles me as I sure can unquote the whole do/block into the module body, but then, how I could control from which command is a subcommand/flag? I mean, all flags and subcommands would be defined as global and then I lose the control I had with the

defcommand “blabla” do
  # flags
  # subcommands
end

Syntax :confused:. In this case I don’t have any other way besides changing/parsing the AST, am I right?

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.