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!
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
.
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
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 Nexuswould set up a@before_compile- then each call to
defcommandwould accumulate details in module attributes - then the
before_compilecould generate the@typeanddefstructlines using all the accumulated commands







