├── ibuilder ├── README.md ├── tests │ ├── not_compile │ │ ├── not_derived.rs │ │ ├── unknown_type.rs │ │ ├── many_fields_struct.rs │ │ ├── unnamed_hidden.rs │ │ ├── invalid_rename.stderr │ │ ├── all_variants_hidden.rs │ │ ├── invalid_rename.rs │ │ ├── many_fields_struct.stderr │ │ ├── duplicated_default.stderr │ │ ├── unnamed_hidden.stderr │ │ ├── invalid_field_type.rs │ │ ├── rename_enum.rs │ │ ├── all_variants_hidden.stderr │ │ ├── incompatible_default.rs │ │ ├── struct_default.stderr │ │ ├── duplicated_default.rs │ │ ├── duplicated_default_variant.rs │ │ ├── field_not_derived.rs │ │ ├── rename_enum.stderr │ │ ├── hidden_no_default.rs │ │ ├── incompatible_default.stderr │ │ ├── struct_default.rs │ │ ├── empty_struct.rs │ │ ├── duplicated_default_variant.stderr │ │ ├── not_derived.stderr │ │ ├── field_not_derived.stderr │ │ ├── invalid_field_type.stderr │ │ ├── empty_struct.stderr │ │ ├── unknown_attribute2.rs │ │ ├── unknown_attribute3.rs │ │ ├── unknown_attribute.rs │ │ ├── unknown_attribute2.stderr │ │ ├── unknown_attribute3.stderr │ │ ├── unknown_attribute.stderr │ │ ├── unknown_type.stderr │ │ └── hidden_no_default.stderr │ ├── compile │ │ ├── non_field_struct.rs │ │ ├── default_builder.rs │ │ ├── simple.rs │ │ ├── all_fields_hidden.rs │ │ ├── default.rs │ │ ├── enum.rs │ │ ├── vec.rs │ │ ├── option.rs │ │ ├── nested_struct.rs │ │ ├── inter_mods.rs │ │ ├── plain_types.rs │ │ ├── public_api.rs │ │ ├── mixed_struct_enum.rs │ │ └── long_struct.rs │ ├── unnamed_field.rs │ ├── trybuild.rs │ ├── enum.rs │ ├── default_variant.rs │ ├── default.rs │ ├── enum_prompt.rs │ ├── struct_prompt.rs │ ├── utils.rs │ ├── random.rs │ ├── hidden.rs │ ├── rename.rs │ └── interaction.rs ├── Cargo.toml └── src │ ├── nodes.rs │ ├── lib.rs │ └── builders.rs ├── .gitignore ├── Cargo.toml ├── docs ├── example1.png ├── example2.png └── example3.png ├── tools ├── update-readme.sh └── version-bump.sh ├── .github └── workflows │ ├── Audit.yml │ └── Rust.yml ├── ibuilder_derive ├── Cargo.toml ├── src │ ├── struct_gen │ │ ├── struct_buildable_value_gen.rs │ │ ├── unnamed_fields.rs │ │ ├── named_fields.rs │ │ └── mod.rs │ ├── lib.rs │ └── enum_gen │ │ ├── enum_buildable_value_gen.rs │ │ └── mod.rs └── README.md ├── LICENCE └── README.md /ibuilder/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["ibuilder", "ibuilder_derive"] 3 | -------------------------------------------------------------------------------- /docs/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edomora97/ibuilder/HEAD/docs/example1.png -------------------------------------------------------------------------------- /docs/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edomora97/ibuilder/HEAD/docs/example2.png -------------------------------------------------------------------------------- /docs/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edomora97/ibuilder/HEAD/docs/example3.png -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/not_derived.rs: -------------------------------------------------------------------------------- 1 | struct Foo { 2 | field: i64, 3 | } 4 | 5 | fn main() { 6 | Foo::builder(); 7 | } 8 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_type.rs: -------------------------------------------------------------------------------- 1 | struct Foo { 2 | field: Bar, 3 | } 4 | 5 | fn main() { 6 | Foo::builder(); 7 | } 8 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/non_field_struct.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo(i64); 5 | 6 | fn main() { 7 | Foo::builder(); 8 | } 9 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/many_fields_struct.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo(i64, String); 5 | 6 | fn main() { 7 | Foo::builder(); 8 | } 9 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unnamed_hidden.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo(#[ibuilder(hidden)] i64); 5 | 6 | fn main() { 7 | Foo::builder(); 8 | } 9 | -------------------------------------------------------------------------------- /ibuilder/tests/unnamed_field.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(Debug, IBuilder)] 4 | struct Test(i64); 5 | 6 | #[test] 7 | fn test() { 8 | Test::builder(); 9 | } 10 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/invalid_rename.stderr: -------------------------------------------------------------------------------- 1 | error: expecting a string 2 | --> $DIR/invalid_rename.rs:5:25 3 | | 4 | 5 | #[ibuilder(rename = 42)] 5 | | ^^ 6 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/all_variants_hidden.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::IBuilder; 2 | 3 | #[derive(IBuilder)] 4 | enum Enum { 5 | #[ibuilder(hidden)] 6 | Var1, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/invalid_rename.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::IBuilder; 2 | 3 | #[derive(IBuilder)] 4 | enum Enum { 5 | #[ibuilder(rename = 42)] 6 | Var1, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /ibuilder/tests/trybuild.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_trybuild() { 3 | let t = trybuild::TestCases::new(); 4 | t.pass("tests/compile/*.rs"); 5 | t.compile_fail("tests/not_compile/*.rs"); 6 | } 7 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/default_builder.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: u8, 6 | } 7 | 8 | fn main() { 9 | Builder::::default(); 10 | } 11 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/simple.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: i32, 6 | } 7 | 8 | fn main() { 9 | let _builder = Foo::builder(); 10 | } 11 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/many_fields_struct.stderr: -------------------------------------------------------------------------------- 1 | error: structs with unnamed fields must have only one field 2 | --> $DIR/many_fields_struct.rs:4:8 3 | | 4 | 4 | struct Foo(i64, String); 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/all_fields_hidden.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::IBuilder; 2 | 3 | #[derive(IBuilder)] 4 | struct Struct { 5 | #[ibuilder(hidden, default = 42)] 6 | field: i32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/duplicated_default.stderr: -------------------------------------------------------------------------------- 1 | error: duplicated default 2 | --> $DIR/duplicated_default.rs:5:30 3 | | 4 | 5 | #[ibuilder(default = 42, default = 42)] 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unnamed_hidden.stderr: -------------------------------------------------------------------------------- 1 | error: unnamed fields cannot be hidden 2 | --> $DIR/unnamed_hidden.rs:4:12 3 | | 4 | 4 | struct Foo(#[ibuilder(hidden)] i64); 5 | | ^^^^^^^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/invalid_field_type.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | field: i64, 6 | ups: &'static str, 7 | } 8 | 9 | fn main() { 10 | Foo::builder(); 11 | } 12 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/rename_enum.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | #[ibuilder(rename = "it doesn't make sense since this name is not shown")] 5 | enum Baz { 6 | Var, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tools/update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -d ./tools ] || (echo "Run this script for the repo root" >&2; exit 1) 4 | 5 | cargo readme -r ibuilder > README.md 6 | cargo readme -r ibuilder_derive > ibuilder_derive/README.md 7 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | #[ibuilder(default = 42)] 6 | bar: u8, 7 | } 8 | 9 | fn main() { 10 | let _builder = Foo::builder(); 11 | } 12 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/all_variants_hidden.stderr: -------------------------------------------------------------------------------- 1 | error: all the variants are hidden 2 | --> $DIR/all_variants_hidden.rs:4:1 3 | | 4 | 4 | / enum Enum { 5 | 5 | | #[ibuilder(hidden)] 6 | 6 | | Var1, 7 | 7 | | } 8 | | |_^ 9 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/incompatible_default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(default = -123)] 6 | field: u64, 7 | } 8 | 9 | fn main() { 10 | Foo::builder(); 11 | } 12 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/struct_default.stderr: -------------------------------------------------------------------------------- 1 | error: default value is supported only on plain types 2 | --> $DIR/struct_default.rs:5:5 3 | | 4 | 5 | / #[ibuilder(default = 42)] 5 | 6 | | field: Bar, 6 | | |______________^ 7 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/duplicated_default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(default = 42, default = 42)] 6 | field: i64, 7 | } 8 | 9 | fn main() { 10 | Foo::builder(); 11 | } 12 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/duplicated_default_variant.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub enum Enum { 5 | #[ibuilder(default)] 6 | Var1, 7 | #[ibuilder(default)] 8 | Var2, 9 | Var3, 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/field_not_derived.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | field: i64, 6 | ups: Bar, 7 | } 8 | 9 | struct Bar { 10 | baz: i32, 11 | } 12 | 13 | fn main() { 14 | Foo::builder(); 15 | } 16 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/rename_enum.stderr: -------------------------------------------------------------------------------- 1 | error: renaming an enum is not supported since the name is not exposed 2 | --> $DIR/rename_enum.rs:4:12 3 | | 4 | 4 | #[ibuilder(rename = "it doesn't make sense since this name is not shown")] 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/enum.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | enum Foo { 5 | Var, 6 | WithFields { 7 | lol: i32, 8 | #[ibuilder(default = "ciao")] 9 | baz: String, 10 | }, 11 | } 12 | 13 | fn main() { 14 | Foo::builder(); 15 | } 16 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/vec.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: Vec, 6 | baz: Vec, 7 | } 8 | 9 | #[derive(Debug, IBuilder)] 10 | pub struct Bim { 11 | lol: i32, 12 | } 13 | 14 | fn main() { 15 | let _builder = Foo::builder(); 16 | } 17 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/option.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: Option, 6 | baz: Option, 7 | } 8 | 9 | #[derive(Debug, IBuilder)] 10 | pub struct Bim { 11 | lol: i32, 12 | } 13 | 14 | fn main() { 15 | let _builder = Foo::builder(); 16 | } 17 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/hidden_no_default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct NonDefault { 5 | field: String, 6 | } 7 | 8 | #[derive(IBuilder)] 9 | struct Foo { 10 | #[ibuilder(hidden)] 11 | field: NonDefault, 12 | } 13 | 14 | fn main() { 15 | Foo::builder(); 16 | } 17 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/incompatible_default.stderr: -------------------------------------------------------------------------------- 1 | error[E0600]: cannot apply unary operator `-` to type `u64` 2 | --> $DIR/incompatible_default.rs:5:26 3 | | 4 | 5 | #[ibuilder(default = -123)] 5 | | ^ cannot apply unary operator `-` 6 | | 7 | = note: unsigned values cannot be negated 8 | -------------------------------------------------------------------------------- /ibuilder/tests/enum.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(Debug, IBuilder)] 4 | enum Enum { 5 | Var1, 6 | Var2 { 7 | field: i32, 8 | #[ibuilder(default = "ciao")] 9 | lol: String, 10 | }, 11 | Var3(i32), 12 | } 13 | 14 | #[test] 15 | fn test() { 16 | let _ = Enum::builder(); 17 | } 18 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/struct_default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(default = 42)] 6 | field: Bar, 7 | } 8 | 9 | #[derive(IBuilder)] 10 | struct Bar { 11 | #[ibuilder(default = 42)] 12 | field: i32, 13 | } 14 | 15 | fn main() { 16 | Foo::builder(); 17 | } 18 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/empty_struct.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(Debug, IBuilder)] 4 | struct Empty1; 5 | 6 | #[derive(Debug, IBuilder)] 7 | struct Empty2(); 8 | 9 | #[derive(Debug, IBuilder)] 10 | struct Empty3 {} 11 | 12 | fn main() { 13 | Empty1::builder(); 14 | Empty2::builder(); 15 | Empty3::builder(); 16 | } 17 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/duplicated_default_variant.stderr: -------------------------------------------------------------------------------- 1 | error: at most one variant can be the default 2 | --> $DIR/duplicated_default_variant.rs:4:1 3 | | 4 | 4 | / pub enum Enum { 5 | 5 | | #[ibuilder(default)] 6 | 6 | | Var1, 7 | 7 | | #[ibuilder(default)] 8 | 8 | | Var2, 9 | 9 | | Var3, 10 | 10 | | } 11 | | |_^ 12 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/not_derived.stderr: -------------------------------------------------------------------------------- 1 | error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope 2 | --> $DIR/not_derived.rs:6:10 3 | | 4 | 1 | struct Foo { 5 | | ---------- function or associated item `builder` not found for this 6 | ... 7 | 6 | Foo::builder(); 8 | | ^^^^^^^ function or associated item not found in `Foo` 9 | -------------------------------------------------------------------------------- /.github/workflows/Audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | jobs: 8 | security_audit: 9 | name: Security audit 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions-rs/audit-check@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/nested_struct.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: Bar, 6 | } 7 | 8 | #[derive(IBuilder)] 9 | pub struct Bar { 10 | baz: Baz, 11 | } 12 | 13 | #[derive(IBuilder)] 14 | pub struct Baz { 15 | bim: Bim, 16 | } 17 | 18 | #[derive(IBuilder)] 19 | pub struct Bim { 20 | val: String, 21 | } 22 | 23 | fn main() { 24 | Foo::builder(); 25 | } 26 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/inter_mods.rs: -------------------------------------------------------------------------------- 1 | mod mod1 { 2 | use ibuilder::*; 3 | 4 | #[derive(IBuilder)] 5 | pub struct Foo { 6 | bar: super::mod2::Bar, 7 | } 8 | } 9 | 10 | mod mod2 { 11 | use ibuilder::*; 12 | 13 | #[derive(IBuilder)] 14 | pub struct Bar { 15 | field: i64, 16 | } 17 | } 18 | 19 | fn main() { 20 | use ibuilder::Buildable; 21 | let _builder = mod1::Foo::builder(); 22 | } 23 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/field_not_derived.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `Bar: NewBuildableValue` is not satisfied 2 | --> $DIR/field_not_derived.rs:3:10 3 | | 4 | 3 | #[derive(IBuilder)] 5 | | ^^^^^^^^ the trait `NewBuildableValue` is not implemented for `Bar` 6 | | 7 | = note: required by `new_buildable_value` 8 | = note: this error originates in the derive macro `IBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 9 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/invalid_field_type.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `&'static str: NewBuildableValue` is not satisfied 2 | --> $DIR/invalid_field_type.rs:3:10 3 | | 4 | 3 | #[derive(IBuilder)] 5 | | ^^^^^^^^ the trait `NewBuildableValue` is not implemented for `&'static str` 6 | | 7 | = note: required by `new_buildable_value` 8 | = note: this error originates in the derive macro `IBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 9 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/empty_struct.stderr: -------------------------------------------------------------------------------- 1 | error: structs with unnamed fields must have only one field 2 | --> $DIR/empty_struct.rs:4:8 3 | | 4 | 4 | struct Empty1; 5 | | ^^^^^^ 6 | 7 | error: structs with unnamed fields must have only one field 8 | --> $DIR/empty_struct.rs:7:8 9 | | 10 | 7 | struct Empty2(); 11 | | ^^^^^^ 12 | 13 | error: the struct must have at least one field 14 | --> $DIR/empty_struct.rs:10:8 15 | | 16 | 10 | struct Empty3 {} 17 | | ^^^^^^ 18 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute2.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(wibble_monster)] 6 | field: i64, 7 | } 8 | 9 | #[derive(IBuilder)] 10 | #[ibuilder(wibble_monster)] 11 | struct Bar { 12 | field: i64, 13 | } 14 | 15 | #[derive(IBuilder)] 16 | #[ibuilder(wibble_monster)] 17 | enum Baz { 18 | Var, 19 | } 20 | 21 | #[derive(IBuilder)] 22 | enum Bim { 23 | #[ibuilder(wibble_monster)] 24 | Var, 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute3.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(wibble(monster))] 6 | field: i64, 7 | } 8 | 9 | #[derive(IBuilder)] 10 | #[ibuilder(wibble(monster))] 11 | struct Bar { 12 | field: i64, 13 | } 14 | 15 | #[derive(IBuilder)] 16 | #[ibuilder(wibble(monster))] 17 | enum Baz { 18 | Var, 19 | } 20 | 21 | #[derive(IBuilder)] 22 | enum Bim { 23 | #[ibuilder(wibble(monster))] 24 | Var, 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | struct Foo { 5 | #[ibuilder(wibble_monster = 42)] 6 | field: i64, 7 | } 8 | 9 | #[derive(IBuilder)] 10 | #[ibuilder(wibble_monster = 42)] 11 | struct Bar { 12 | field: i64, 13 | } 14 | 15 | #[derive(IBuilder)] 16 | #[ibuilder(wibble_monster = 42)] 17 | enum Baz { 18 | Var, 19 | } 20 | 21 | #[derive(IBuilder)] 22 | enum Bim { 23 | #[ibuilder(wibble_monster = 42)] 24 | Var, 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/plain_types.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | use std::path::PathBuf; 3 | 4 | #[derive(IBuilder)] 5 | pub struct Foo { 6 | f_i8: i8, 7 | f_i16: i16, 8 | f_i32: i32, 9 | f_i64: i64, 10 | f_u8: u8, 11 | f_u16: u16, 12 | f_u32: u32, 13 | f_u64: u64, 14 | f_isize: isize, 15 | f_usize: usize, 16 | f_f32: f32, 17 | f_f64: f64, 18 | f_string: String, 19 | f_char: char, 20 | f_pathbuf: PathBuf, 21 | f_bool: bool, 22 | } 23 | 24 | fn main() { 25 | let _builder = Foo::builder(); 26 | } 27 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/public_api.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::nodes::Node; 2 | use ibuilder::*; 3 | 4 | #[derive(IBuilder)] 5 | pub struct Foo { 6 | bar: i32, 7 | } 8 | 9 | fn main() { 10 | let mut builder: Builder = Foo::builder(); 11 | let _: Options = builder.get_options(); 12 | let _: Result, ChooseError> = builder.choose(Input::text("foo")); 13 | let _: Result, ChooseError> = builder.choose(Input::choice("foo")); 14 | let _: Result = builder.finalize(); 15 | let _: bool = builder.is_done(); 16 | let _: Node = builder.to_node(); 17 | } 18 | -------------------------------------------------------------------------------- /ibuilder/tests/default_variant.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub enum NotDefaulted { 5 | Var1, 6 | Var2, 7 | } 8 | 9 | #[derive(IBuilder, Eq, PartialEq, Debug)] 10 | pub enum Defaulted { 11 | #[ibuilder(default)] 12 | Var1, 13 | Var2, 14 | } 15 | 16 | #[test] 17 | fn test_no_default() { 18 | let builder = NotDefaulted::builder(); 19 | assert!(!builder.is_done()); 20 | } 21 | 22 | #[test] 23 | fn test_default() { 24 | let builder = Defaulted::builder(); 25 | assert!(builder.is_done()); 26 | let res = builder.finalize().unwrap(); 27 | assert_eq!(res, Defaulted::Var1); 28 | } 29 | -------------------------------------------------------------------------------- /ibuilder/tests/default.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct NestedFoo { 5 | foo: Foo, 6 | } 7 | 8 | #[derive(IBuilder)] 9 | pub struct Foo { 10 | #[ibuilder(default = 42)] 11 | bar: i32, 12 | } 13 | 14 | #[test] 15 | fn test_default() { 16 | let builder = Foo::builder(); 17 | assert!(builder.is_done()); 18 | let fooo = builder.finalize().unwrap(); 19 | assert_eq!(fooo.bar, 42); 20 | } 21 | 22 | #[test] 23 | fn test_nested_default() { 24 | let builder = NestedFoo::builder(); 25 | assert!(builder.is_done()); 26 | let fooo = builder.finalize().unwrap(); 27 | assert_eq!(fooo.foo.bar, 42); 28 | } 29 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute2.stderr: -------------------------------------------------------------------------------- 1 | error: unknown attribute 2 | --> $DIR/unknown_attribute2.rs:5:16 3 | | 4 | 5 | #[ibuilder(wibble_monster)] 5 | | ^^^^^^^^^^^^^^ 6 | 7 | error: unknown attribute 8 | --> $DIR/unknown_attribute2.rs:10:12 9 | | 10 | 10 | #[ibuilder(wibble_monster)] 11 | | ^^^^^^^^^^^^^^ 12 | 13 | error: unknown attribute 14 | --> $DIR/unknown_attribute2.rs:16:12 15 | | 16 | 16 | #[ibuilder(wibble_monster)] 17 | | ^^^^^^^^^^^^^^ 18 | 19 | error: unknown attribute 20 | --> $DIR/unknown_attribute2.rs:23:16 21 | | 22 | 23 | #[ibuilder(wibble_monster)] 23 | | ^^^^^^^^^^^^^^ 24 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute3.stderr: -------------------------------------------------------------------------------- 1 | error: unknown attribute 2 | --> $DIR/unknown_attribute3.rs:5:16 3 | | 4 | 5 | #[ibuilder(wibble(monster))] 5 | | ^^^^^^^^^^^^^^^ 6 | 7 | error: unknown attribute 8 | --> $DIR/unknown_attribute3.rs:10:12 9 | | 10 | 10 | #[ibuilder(wibble(monster))] 11 | | ^^^^^^^^^^^^^^^ 12 | 13 | error: unknown attribute 14 | --> $DIR/unknown_attribute3.rs:16:12 15 | | 16 | 16 | #[ibuilder(wibble(monster))] 17 | | ^^^^^^^^^^^^^^^ 18 | 19 | error: unknown attribute 20 | --> $DIR/unknown_attribute3.rs:23:16 21 | | 22 | 23 | #[ibuilder(wibble(monster))] 23 | | ^^^^^^^^^^^^^^^ 24 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: unknown attribute 2 | --> $DIR/unknown_attribute.rs:5:16 3 | | 4 | 5 | #[ibuilder(wibble_monster = 42)] 5 | | ^^^^^^^^^^^^^^ 6 | 7 | error: unknown attribute 8 | --> $DIR/unknown_attribute.rs:10:12 9 | | 10 | 10 | #[ibuilder(wibble_monster = 42)] 11 | | ^^^^^^^^^^^^^^ 12 | 13 | error: unknown attribute 14 | --> $DIR/unknown_attribute.rs:16:12 15 | | 16 | 16 | #[ibuilder(wibble_monster = 42)] 17 | | ^^^^^^^^^^^^^^ 18 | 19 | error: unknown attribute 20 | --> $DIR/unknown_attribute.rs:23:16 21 | | 22 | 23 | #[ibuilder(wibble_monster = 42)] 23 | | ^^^^^^^^^^^^^^ 24 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/mixed_struct_enum.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::*; 2 | 3 | #[derive(IBuilder)] 4 | pub struct Foo { 5 | bar: Bar, 6 | bim: Box, 7 | } 8 | 9 | #[derive(IBuilder)] 10 | pub struct Bar { 11 | baz: Baz, 12 | } 13 | 14 | #[derive(IBuilder)] 15 | pub struct Baz(Bim); 16 | 17 | #[derive(IBuilder)] 18 | pub struct Bim { 19 | val: Vec, 20 | } 21 | 22 | #[derive(IBuilder)] 23 | pub enum Enum { 24 | Var1, 25 | Var2(String), 26 | Var3(Bar), 27 | Var4 { foo: Foo, lol: i32 }, 28 | Var5(Enumeration), 29 | } 30 | 31 | #[derive(IBuilder)] 32 | pub enum Enumeration { 33 | Var1, 34 | Var2(Vec>), 35 | } 36 | 37 | fn main() { 38 | Foo::builder(); 39 | } 40 | -------------------------------------------------------------------------------- /ibuilder_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ibuilder_derive" 3 | version = "0.1.8" 4 | authors = ["Edoardo Morassutto "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Interactive builder for Rust types" 8 | homepage = "https://github.com/edomora97/ibuilder" 9 | repository = "https://github.com/edomora97/ibuilder" 10 | keywords = ["builder", "derive", "interactive"] 11 | categories = ["config", "data-structures", "parsing"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | syn = { version = "1.0", features = ["extra-traits"] } 16 | proc-macro2 = "1.0" 17 | quote = "1.0" 18 | proc-macro-error = "1.0" 19 | 20 | [dev-dependencies] 21 | ibuilder = { path = "../ibuilder" } 22 | 23 | [lib] 24 | proc-macro = true 25 | path = "src/lib.rs" 26 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/unknown_type.stderr: -------------------------------------------------------------------------------- 1 | error[E0412]: cannot find type `Bar` in this scope 2 | --> $DIR/unknown_type.rs:2:12 3 | | 4 | 2 | field: Bar, 5 | | ^^^ not found in this scope 6 | 7 | error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope 8 | --> $DIR/unknown_type.rs:6:10 9 | | 10 | 1 | struct Foo { 11 | | ---------- function or associated item `builder` not found for this 12 | ... 13 | 6 | Foo::builder(); 14 | | ^^^^^^^ function or associated item not found in `Foo` 15 | | 16 | = help: items from traits can only be used if the trait is implemented and in scope 17 | = note: the following trait defines an item `builder`, perhaps you need to implement it: 18 | candidate #1: `ibuilder::Buildable` 19 | -------------------------------------------------------------------------------- /ibuilder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ibuilder" 3 | version = "0.1.8" 4 | authors = ["Edoardo Morassutto "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Interactive builder for Rust types" 8 | homepage = "https://github.com/edomora97/ibuilder" 9 | repository = "https://github.com/edomora97/ibuilder" 10 | keywords = ["builder", "derive", "interactive"] 11 | categories = ["config", "data-structures", "parsing"] 12 | readme = "README.md" 13 | 14 | [features] 15 | default = ["derive"] 16 | derive = ["ibuilder_derive"] 17 | 18 | [dependencies] 19 | failure = "0.1" 20 | ibuilder_derive = { path = "../ibuilder_derive", version = "0.1.8", optional = true } 21 | 22 | [dev-dependencies] 23 | trybuild = "1.0" 24 | ibuilder_derive = { path = "../ibuilder_derive", version = "0.1.8" } 25 | rand = "0.7" -------------------------------------------------------------------------------- /ibuilder_derive/src/struct_gen/struct_buildable_value_gen.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | use quote::quote; 4 | 5 | use crate::struct_gen::named_fields::StructWithNamedFields; 6 | use crate::struct_gen::unnamed_fields::StructWithUnnamedFields; 7 | use crate::struct_gen::StructGenerator; 8 | 9 | /// Generate the implementation of `BuildableValue` for the builder struct. 10 | pub fn gen_impl_buildable_value(gen: &StructGenerator) -> TokenStream { 11 | let builder_ident = &gen.builder_ident; 12 | let content = if gen.is_named() { 13 | StructWithNamedFields::new(gen).gen() 14 | } else { 15 | StructWithUnnamedFields::new(gen).gen() 16 | }; 17 | quote! { 18 | #[automatically_derived] 19 | #[allow(unreachable_code)] 20 | impl ibuilder::BuildableValue for #builder_ident { 21 | #content 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ibuilder_derive/README.md: -------------------------------------------------------------------------------- 1 | # ibuilder_derive 2 | 3 | [![crates.io](https://img.shields.io/crates/v/ibuilder_derive.svg)](https://crates.io/crates/ibuilder_derive) 4 | [![Docs](https://docs.rs/ibuilder_derive/badge.svg)](https://docs.rs/ibuilder_derive) 5 | 6 | See the documentation of the [`ibuilder`](https://crates.io/crates/ibuilder) create for the details, 7 | you probably are looking for that. 8 | 9 | ### ibuilder derive macro 10 | 11 | Usage: 12 | ```rust 13 | #[derive(IBuilder)] 14 | struct Example { 15 | field1: i64, 16 | #[ibuilder(default = "something")] 17 | field2: String, 18 | } 19 | ``` 20 | 21 | Will implement the trait `ibuilder::Buildable` for `Example`, prodiding the `builder()` method 22 | for getting a `ibuilder::Builder`. 23 | 24 | It will also implement a private struct for keeping the state of the builder and implement the 25 | `NewBuildableValue` trait for `Example`, allowing it to be inside a fields of other derived 26 | types. 27 | 28 | License: MIT 29 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ibuilder/tests/not_compile/hidden_no_default.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `NonDefault: Default` is not satisfied 2 | --> $DIR/hidden_no_default.rs:8:10 3 | | 4 | 8 | #[derive(IBuilder)] 5 | | ^^^^^^^^ the trait `Default` is not implemented for `NonDefault` 6 | | 7 | = note: required by `std::default::Default::default` 8 | = note: this error originates in the derive macro `IBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 9 | 10 | error[E0599]: no method named `clone` found for struct `NonDefault` in the current scope 11 | --> $DIR/hidden_no_default.rs:8:10 12 | | 13 | 4 | struct NonDefault { 14 | | ----------------- method `clone` not found for this 15 | ... 16 | 8 | #[derive(IBuilder)] 17 | | ^^^^^^^^ method not found in `NonDefault` 18 | | 19 | = help: items from traits can only be used if the trait is implemented and in scope 20 | = note: the following trait defines an item `clone`, perhaps you need to implement it: 21 | candidate #1: `Clone` 22 | = note: this error originates in the derive macro `IBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 23 | -------------------------------------------------------------------------------- /ibuilder/src/nodes.rs: -------------------------------------------------------------------------------- 1 | //! Allow generic display of the structures using a tree representation. 2 | //! 3 | //! The `Builder` exposes the `to_node()` method that returns a tree-like structures with all the 4 | //! visible fields of the builder. This structure can be used for pretty-printing the internal 5 | //! builder state is a customized manner. 6 | 7 | /// A `Node` of the tree, it represents an item that can be interacted with. 8 | #[derive(Debug)] 9 | pub enum Node { 10 | /// The `Node` is a leaf node of the tree, i.e. it doesn't contains subfields, just a value. 11 | Leaf(Field), 12 | /// The `Node` is actually composed by inner fields, for example a `Vec` is composed by items 13 | /// and a `struct` by fields. 14 | Composite(String, Vec), 15 | } 16 | 17 | /// A field of a composite structure. The field may be named (like in `struct`s), or be unnamed 18 | /// (like in `Vec`). 19 | #[derive(Debug)] 20 | pub enum FieldKind { 21 | /// The field is named, the first item is the name of the field, the second is the inner node of 22 | /// it. 23 | Named(String, Node), 24 | /// The field does not have a name, the value is the inner node of the item. 25 | Unnamed(Node), 26 | } 27 | 28 | /// A leaf field of the tree structure. 29 | #[derive(Debug)] 30 | pub enum Field { 31 | /// The field is valid and the textual representation of it is provided. 32 | String(String), 33 | /// The field is not present yet. 34 | Missing, 35 | } 36 | -------------------------------------------------------------------------------- /tools/version-bump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -d ./tools ] || (echo "Run this script for the repo root" >&2; exit 1) 4 | [ $# -lt 1 ] && (echo "Usage: $0 version" >&2; exit 1) 5 | if [[ "$1" == v* ]]; then 6 | echo "The version should not start with v" >&2 7 | exit 1 8 | fi 9 | 10 | version="$1" 11 | 12 | echo "Updating version of ibuilder_derive to $version" 13 | sed -i "s/^version =.*/version = \"${version}\"/" "ibuilder_derive/Cargo.toml" 14 | 15 | echo "Updating version of ibuilder to $version" 16 | sed -i "s/^version =.*/version = \"${version}\"/" "ibuilder/Cargo.toml" 17 | sed -Ei "s/(ibuilder_derive.+version = )\"[^\"]+\"(.*)/\\1\"${version}\"\\2/" "ibuilder/Cargo.toml" 18 | 19 | echo "Running cargo test" 20 | cargo test --all 21 | 22 | echo "Committing changes" 23 | git add ibuilder/Cargo.toml ibuilder_derive/Cargo.toml 24 | git commit -m "Version v${version}" 25 | 26 | echo "Publishing ibuilder_derive" 27 | (cd ibuilder_derive && cargo publish) 28 | 29 | echo "Waiting a bit for crates.io" 30 | found=no 31 | for i in {1..20}; do 32 | echo "Attempt $i" 33 | actual=$(cargo search ibuilder_derive | head -n 1 | cut -d'"' -f 2) 34 | echo "crates.io reports version ${actual}" 35 | if [[ $actual == "${version}" ]]; then 36 | found=yes 37 | break 38 | fi 39 | echo "attempting again in 5s" 40 | sleep 5s 41 | done 42 | if [[ $found == no ]]; then 43 | echo "crates.io hasn't updated the version :c" 44 | fi 45 | 46 | echo "Publishing ibuilder" 47 | (cd ibuilder && cargo publish) 48 | -------------------------------------------------------------------------------- /ibuilder/tests/compile/long_struct.rs: -------------------------------------------------------------------------------- 1 | use ibuilder::{Buildable, IBuilder}; 2 | 3 | #[derive(IBuilder)] 4 | struct Looooooong { 5 | field0: i64, 6 | field1: i64, 7 | field2: i64, 8 | field3: i64, 9 | field4: i64, 10 | field5: i64, 11 | field6: i64, 12 | field7: i64, 13 | field8: i64, 14 | field9: i64, 15 | field10: i64, 16 | field11: i64, 17 | field12: i64, 18 | field13: i64, 19 | field14: i64, 20 | field15: i64, 21 | field16: i64, 22 | field17: i64, 23 | field18: i64, 24 | field19: i64, 25 | field20: i64, 26 | field21: i64, 27 | field22: i64, 28 | field23: i64, 29 | field24: i64, 30 | field25: i64, 31 | field26: i64, 32 | field27: i64, 33 | field28: i64, 34 | field29: i64, 35 | field30: i64, 36 | field31: i64, 37 | field32: i64, 38 | field33: i64, 39 | field34: i64, 40 | field35: i64, 41 | field36: i64, 42 | field37: i64, 43 | field38: i64, 44 | field39: i64, 45 | field40: i64, 46 | field41: i64, 47 | field42: i64, 48 | field43: i64, 49 | field44: i64, 50 | field45: i64, 51 | field46: i64, 52 | field47: i64, 53 | field48: i64, 54 | field49: i64, 55 | field50: i64, 56 | field51: i64, 57 | field52: i64, 58 | field53: i64, 59 | field54: i64, 60 | field55: i64, 61 | field56: i64, 62 | field57: i64, 63 | field58: i64, 64 | field59: i64, 65 | field60: i64, 66 | field61: i64, 67 | field62: i64, 68 | field63: i64, 69 | field64: i64, 70 | } 71 | 72 | fn main() { 73 | Looooooong::builder(); 74 | } 75 | -------------------------------------------------------------------------------- /ibuilder/tests/enum_prompt.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ibuilder::*; 4 | 5 | #[derive(IBuilder)] 6 | enum DefaultPrompt { 7 | Var, 8 | } 9 | 10 | #[derive(IBuilder)] 11 | #[ibuilder(prompt = "lol or lel?")] 12 | enum CustomPrompt { 13 | Var1, 14 | #[ibuilder(prompt = "override unnamed")] 15 | Var2(Nested), 16 | #[ibuilder(prompt = "custom prompt")] 17 | Var3 { 18 | #[ibuilder(prompt = "override field")] 19 | field: Nested, 20 | }, 21 | } 22 | 23 | #[derive(IBuilder)] 24 | #[ibuilder(prompt = "base prompt")] 25 | struct Nested { 26 | field: i32, 27 | } 28 | 29 | #[test] 30 | fn default_prompt() { 31 | let mut builder = DefaultPrompt::builder(); 32 | 33 | let options = builder.get_options(); 34 | assert!(!options.query.is_empty()); 35 | 36 | builder.choose(Input::choice("Var")).unwrap(); 37 | let options = builder.get_options(); 38 | assert!(!options.query.is_empty()); 39 | } 40 | 41 | #[test] 42 | fn custom_prompt() { 43 | let mut builder = CustomPrompt::builder(); 44 | 45 | let options = builder.get_options(); 46 | assert_eq!(options.query, "lol or lel?"); 47 | 48 | builder.choose(Input::choice("Var2")).unwrap(); 49 | let options = builder.get_options(); 50 | assert_eq!(options.query, "override unnamed"); 51 | 52 | builder.choose(Input::choice(BACK_ID)).unwrap(); 53 | builder.choose(Input::choice("Var3")).unwrap(); 54 | let options = builder.get_options(); 55 | assert_eq!(options.query, "custom prompt"); 56 | 57 | builder.choose(Input::choice("field")).unwrap(); 58 | let options = builder.get_options(); 59 | assert_eq!(options.query, "override field"); 60 | } 61 | -------------------------------------------------------------------------------- /ibuilder/tests/struct_prompt.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ibuilder::*; 4 | 5 | #[derive(IBuilder)] 6 | struct DefaultPrompt { 7 | field: i32, 8 | } 9 | 10 | #[derive(IBuilder)] 11 | #[ibuilder(prompt = "lol or lel?")] 12 | struct UnnamedStruct(String); 13 | 14 | #[derive(IBuilder)] 15 | #[ibuilder(prompt = "lol or lel?")] 16 | struct CustomPrompt { 17 | #[ibuilder(prompt = "plain field prompt")] 18 | field: i32, 19 | #[ibuilder(prompt = "override enum")] 20 | var: Enum, 21 | #[ibuilder(prompt = "override struct")] 22 | nest: Nested, 23 | } 24 | 25 | #[derive(IBuilder)] 26 | #[ibuilder(prompt = "base prompt")] 27 | struct Nested { 28 | field: i32, 29 | } 30 | 31 | #[derive(IBuilder)] 32 | #[ibuilder(prompt = "base prompt")] 33 | enum Enum { 34 | Var, 35 | } 36 | 37 | #[test] 38 | fn default_prompt() { 39 | let mut builder = DefaultPrompt::builder(); 40 | 41 | let options = builder.get_options(); 42 | assert!(!options.query.is_empty()); 43 | 44 | builder.choose(Input::choice("field")).unwrap(); 45 | let options = builder.get_options(); 46 | assert!(!options.query.is_empty()); 47 | } 48 | 49 | #[test] 50 | fn unnamed_prompt() { 51 | let builder = UnnamedStruct::builder(); 52 | 53 | let options = builder.get_options(); 54 | assert_eq!(options.query, "lol or lel?"); 55 | } 56 | 57 | #[test] 58 | fn custom_prompt() { 59 | let mut builder = CustomPrompt::builder(); 60 | 61 | let options = builder.get_options(); 62 | assert_eq!(options.query, "lol or lel?"); 63 | 64 | builder.choose(Input::choice("field")).unwrap(); 65 | let options = builder.get_options(); 66 | assert_eq!(options.query, "plain field prompt"); 67 | 68 | builder.choose(Input::choice(BACK_ID)).unwrap(); 69 | builder.choose(Input::choice("var")).unwrap(); 70 | let options = builder.get_options(); 71 | assert_eq!(options.query, "override enum"); 72 | 73 | builder.choose(Input::choice(BACK_ID)).unwrap(); 74 | builder.choose(Input::choice("nest")).unwrap(); 75 | let options = builder.get_options(); 76 | assert_eq!(options.query, "override struct"); 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/Rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | clippy: 7 | name: Clippy 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Install Rust 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | components: clippy 16 | override: true 17 | - name: Cargo clippy 18 | uses: actions-rs/clippy-check@v1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | args: --all-targets --all-features --tests --all -- -D warnings 22 | rustfmt: 23 | name: Format 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v1 27 | - name: Install Rust 28 | id: component 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | components: rustfmt 33 | override: true 34 | - name: Run cargo fmt 35 | uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: --all -- --check 39 | readme: 40 | name: Readme 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v1 44 | - name: Install Rust 45 | id: component 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: stable 49 | components: rustfmt 50 | override: true 51 | - name: Install cargo-readme 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: install 55 | args: cargo-readme 56 | - name: Rebuild readme 57 | run: ./tools/update-readme.sh 58 | - name: Check no changes 59 | run: | 60 | git diff --exit-code || (echo 'Readme is outdated!' >&2; exit 1) 61 | test: 62 | name: Test 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v1 66 | - name: Install Rust 67 | id: component 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | toolchain: stable 71 | components: rustfmt 72 | override: true 73 | - name: Run cargo test 74 | uses: actions-rs/cargo@v1 75 | with: 76 | command: test 77 | args: --color=always --all --no-fail-fast 78 | env: 79 | RUST_BACKTRACE: 1 -------------------------------------------------------------------------------- /ibuilder/tests/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::io::{BufRead, Write}; 4 | 5 | use failure::Error; 6 | 7 | use ibuilder::nodes::*; 8 | use ibuilder::*; 9 | 10 | /// Pass a `Builder` to this function to use an interactive console inspecting the behaviour of the 11 | /// builder. You may want to add the `--nocapture` option to see the output of this function. 12 | pub fn interactive_console(mut builder: Builder) -> Result { 13 | let stdin = std::io::stdin(); 14 | let mut iterator = stdin.lock().lines(); 15 | 16 | loop { 17 | println!("\n\n\n"); 18 | write_node(builder.to_node(), &mut std::io::stdout(), 0)?; 19 | let options = builder.get_options(); 20 | println!("\n?: {}", options.query); 21 | for opt in &options.choices { 22 | println!( 23 | "- {}{} ({})", 24 | opt.text, 25 | if opt.needs_action { "*" } else { "" }, 26 | opt.choice_id 27 | ); 28 | } 29 | if options.text_input { 30 | println!("- textual input (> followed by the content)"); 31 | } 32 | let line = iterator.next().unwrap()?; 33 | let input = if let Some(stripped) = line.strip_prefix('>') { 34 | Input::Text(stripped.to_string()) 35 | } else { 36 | Input::Choice(line) 37 | }; 38 | match builder.choose(input) { 39 | Ok(Some(res)) => return Ok(res), 40 | Ok(None) => {} 41 | Err(e) => println!("\n{}\n", e), 42 | } 43 | } 44 | } 45 | 46 | pub fn write_node(node: Node, w: &mut W, indent: usize) -> Result<(), Error> { 47 | let pad = " ".repeat(indent); 48 | match node { 49 | Node::Composite(name, fields) => { 50 | w.write_all(format!("{}\n", name).as_bytes())?; 51 | for field in fields { 52 | match field { 53 | FieldKind::Named(name, field) => { 54 | w.write_all(format!("{}- {}: ", pad, name).as_bytes())?; 55 | write_node(field, w, indent + 1)?; 56 | } 57 | FieldKind::Unnamed(field) => { 58 | w.write_all(format!("{}- ", pad).as_bytes())?; 59 | write_node(field, w, indent + 1)?; 60 | } 61 | } 62 | } 63 | } 64 | Node::Leaf(field) => match field { 65 | Field::String(content) => w.write_all(format!("{}\n", content).as_bytes())?, 66 | Field::Missing => w.write_all(b"missing\n")?, 67 | }, 68 | } 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /ibuilder/tests/random.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::box_vec, clippy::type_complexity)] 2 | 3 | use ibuilder::*; 4 | use rand::prelude::*; 5 | 6 | #[derive(Debug, IBuilder)] 7 | struct Base { 8 | integer: i32, 9 | #[ibuilder(default = 42)] 10 | defaulted: i32, 11 | inner: Inner, 12 | #[ibuilder(rename = "enum")] 13 | en: Enum, 14 | } 15 | 16 | #[derive(Debug, IBuilder)] 17 | #[ibuilder(rename = "inner inner inner")] 18 | struct Inner { 19 | string: Option, 20 | #[ibuilder(default = "lol")] 21 | defaulted: String, 22 | } 23 | 24 | #[derive(Debug, IBuilder)] 25 | #[ibuilder(prompt = "WHAAT??!")] 26 | enum Enum { 27 | #[ibuilder(rename = "hello")] 28 | Var1, 29 | Var2 { 30 | #[ibuilder(hidden, default = "nope")] 31 | field: String, 32 | #[ibuilder(rename = "baz")] 33 | field2: Inner, 34 | }, 35 | Var3(Inner), 36 | #[ibuilder(rename = "man! this field is strange!")] 37 | Var4(Vec>>>>>>), 38 | } 39 | 40 | #[test] 41 | fn random() { 42 | let mut builder = Base::builder(); 43 | const N_ITER: usize = 10_000; 44 | let mut rng = rand::thread_rng(); 45 | 46 | for _ in 0..N_ITER { 47 | let options = builder.get_options(); 48 | if options.text_input && rand::random() { 49 | let input = rand::random::().to_string(); 50 | builder.choose(Input::text(&input)).unwrap_or_else(|e| { 51 | panic!( 52 | "Failed to choose text '{}': {}\nBuilder: {:#?}", 53 | input, e, builder 54 | ) 55 | }); 56 | } else if rand::random() { 57 | let res = builder.choose(Input::choice("totally not a valid choice")); 58 | if res.as_ref().unwrap_err() != &ChooseError::UnexpectedChoice { 59 | panic!( 60 | "Expecting ChooseError::UnexpectedChoice, but got: {:?}", 61 | res 62 | ); 63 | } 64 | let options = builder.get_options(); 65 | if !options.text_input { 66 | let res = builder.choose(Input::text("surprise! some text!")); 67 | if res.as_ref().unwrap_err() != &ChooseError::UnexpectedText { 68 | panic!("Expecting ChooseError::UnexpectedText, but got: {:?}", res); 69 | } 70 | } 71 | } else { 72 | let choice = options.choices.choose(&mut rng).expect("Empty choices"); 73 | builder 74 | .choose(Input::choice(&choice.choice_id)) 75 | .unwrap_or_else(|e| { 76 | panic!( 77 | "Failed to choose option {:?}: {}\nBuilder: {:#?}", 78 | choice, e, builder 79 | ) 80 | }); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ibuilder_derive/src/struct_gen/unnamed_fields.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use proc_macro_error::abort; 3 | 4 | use quote::quote; 5 | 6 | use crate::struct_gen::StructGenerator; 7 | 8 | /// The generator of the implementation of `BuildableValue` for a struct with named fields. 9 | pub struct StructWithUnnamedFields<'s> { 10 | /// The base struct generator. 11 | gen: &'s StructGenerator, 12 | } 13 | 14 | impl<'s> StructWithUnnamedFields<'s> { 15 | /// Make a new `StructWithNamedFields` from a `StructGenerator`. 16 | pub fn new(gen: &'s StructGenerator) -> Self { 17 | if gen.fields.len() != 1 { 18 | abort!( 19 | gen.span, 20 | "structs with unnamed fields must have only one field" 21 | ); 22 | } 23 | Self { gen } 24 | } 25 | 26 | /// Generate the implementation of the trait methods. 27 | pub fn gen(&self) -> TokenStream { 28 | let fn_apply = self.gen_fn_apply(); 29 | let fn_get_options = self.gen_fn_get_options(); 30 | let fn_get_subfields = self.gen_fn_get_subfields(); 31 | let fn_to_node = self.gen_fn_to_node(); 32 | let fn_get_value_any = self.gen_fn_get_value_any(); 33 | quote! { 34 | #fn_apply 35 | #fn_get_options 36 | #fn_get_subfields 37 | #fn_to_node 38 | #fn_get_value_any 39 | } 40 | } 41 | 42 | /// Generate the implementation of the `apply` method. 43 | fn gen_fn_apply(&self) -> TokenStream { 44 | quote! { 45 | fn apply(&mut self, data: ibuilder::Input, current_fields: &[String]) -> Result<(), ibuilder::ChooseError> { 46 | self.0.apply(data, current_fields) 47 | } 48 | } 49 | } 50 | 51 | /// Generate the implementation of the `get_options` method. 52 | fn gen_fn_get_options(&self) -> TokenStream { 53 | quote! { 54 | fn get_options(&self, current_fields: &[String]) -> ibuilder::Options { 55 | self.0.get_options(current_fields) 56 | } 57 | } 58 | } 59 | 60 | /// Generate the implementation of the `get_subfields` method. 61 | fn gen_fn_get_subfields(&self) -> TokenStream { 62 | quote! { 63 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 64 | self.0.get_subfields(current_fields) 65 | } 66 | } 67 | } 68 | 69 | /// Generate the implementation of the `to_node` method. 70 | fn gen_fn_to_node(&self) -> TokenStream { 71 | let name = self.gen.actual_name(); 72 | quote! { 73 | fn to_node(&self) -> ibuilder::nodes::Node { 74 | ibuilder::nodes::Node::Composite( 75 | #name.to_string(), 76 | vec![ibuilder::nodes::FieldKind::Unnamed(self.0.to_node())], 77 | ) 78 | } 79 | } 80 | } 81 | 82 | /// Generate the implementation of the `get_value_any` method. 83 | fn gen_fn_get_value_any(&self) -> TokenStream { 84 | let ident = &self.gen.ident; 85 | quote! { 86 | fn get_value_any(&self) -> Option> { 87 | Some(Box::new(#ident(*self.0.get_value_any()?.downcast().unwrap()))) 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ibuilder/tests/hidden.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ibuilder::nodes::{FieldKind, Node}; 4 | use ibuilder::*; 5 | 6 | #[derive(IBuilder, Eq, PartialEq, Debug)] 7 | struct Struct { 8 | #[ibuilder(hidden, default = 42)] 9 | field: i32, 10 | enm: Enum, 11 | } 12 | 13 | #[derive(Clone, Eq, PartialEq, Debug)] 14 | struct Defaultable { 15 | field: String, 16 | } 17 | 18 | impl Default for Defaultable { 19 | fn default() -> Self { 20 | Defaultable { 21 | field: "success".into(), 22 | } 23 | } 24 | } 25 | 26 | #[derive(IBuilder, Eq, PartialEq, Debug)] 27 | struct StructWithoutDefault { 28 | #[ibuilder(hidden)] 29 | field: Defaultable, 30 | field2: i32, 31 | } 32 | 33 | #[derive(IBuilder, Eq, PartialEq, Debug)] 34 | enum Enum { 35 | #[ibuilder(hidden)] 36 | Var1, 37 | Var2 { 38 | field: i32, 39 | }, 40 | Var3(i32), 41 | } 42 | 43 | #[test] 44 | fn hidden_variant() { 45 | let mut builder = Enum::builder(); 46 | 47 | let options = builder.get_options(); 48 | let mut choices = options.choices.iter().map(|c| c.text.as_str()); 49 | assert!(!choices.any(|x| x == "Var1")); 50 | 51 | assert_eq!( 52 | builder.choose(Input::choice("Var1")), 53 | Err(ChooseError::UnexpectedChoice) 54 | ); 55 | } 56 | 57 | #[test] 58 | fn hidden_field() { 59 | let mut builder = Struct::builder(); 60 | 61 | let options = builder.get_options(); 62 | let mut choices = options.choices.iter().map(|c| c.text.as_str()); 63 | assert!(!choices.any(|x| x == "field")); 64 | 65 | assert_eq!( 66 | builder.choose(Input::choice("field")), 67 | Err(ChooseError::UnexpectedChoice) 68 | ); 69 | 70 | let node = builder.to_node(); 71 | match node { 72 | Node::Leaf(_) => panic!("expecting a composite"), 73 | Node::Composite(_, fields) => { 74 | assert_eq!(fields.len(), 1); 75 | match &fields[0] { 76 | FieldKind::Named(name, _) => { 77 | assert_ne!(name, "field"); 78 | } 79 | FieldKind::Unnamed(_) => panic!("expecting named"), 80 | } 81 | } 82 | } 83 | } 84 | 85 | #[test] 86 | fn hidden_field_without_default() { 87 | let mut builder = StructWithoutDefault::builder(); 88 | 89 | let options = builder.get_options(); 90 | let mut choices = options.choices.iter().map(|c| c.text.as_str()); 91 | assert!(!choices.any(|x| x == "field")); 92 | 93 | assert_eq!( 94 | builder.choose(Input::choice("field")), 95 | Err(ChooseError::UnexpectedChoice) 96 | ); 97 | 98 | let node = builder.to_node(); 99 | match node { 100 | Node::Leaf(_) => panic!("expecting a composite"), 101 | Node::Composite(_, fields) => { 102 | assert_eq!(fields.len(), 1); 103 | match &fields[0] { 104 | FieldKind::Named(name, _) => { 105 | assert_ne!(name, "field"); 106 | } 107 | FieldKind::Unnamed(_) => panic!("expecting named"), 108 | } 109 | } 110 | } 111 | 112 | builder.choose(Input::choice("field2")).unwrap(); 113 | builder.choose(Input::text("42")).unwrap(); 114 | 115 | let res = builder.finalize().unwrap(); 116 | assert_eq!(res.field.field, "success"); 117 | } 118 | -------------------------------------------------------------------------------- /ibuilder/tests/rename.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use ibuilder::nodes::{Field, FieldKind, Node}; 4 | use ibuilder::*; 5 | 6 | #[derive(IBuilder)] 7 | #[ibuilder(rename = "Renamed struct")] 8 | struct Struct { 9 | #[ibuilder(rename = "renamed field")] 10 | field: i32, 11 | #[ibuilder(rename = "enum")] 12 | enm: Enum, 13 | } 14 | 15 | #[derive(IBuilder)] 16 | enum Enum { 17 | #[ibuilder(rename = "renamed variant")] 18 | Var1, 19 | #[ibuilder(rename = "renamed variant 2")] 20 | Var2 { 21 | #[ibuilder(rename = "renamed inner field")] 22 | field: i32, 23 | }, 24 | #[ibuilder(rename = "renamed variant 3")] 25 | Var3(i32), 26 | } 27 | 28 | #[test] 29 | fn test_struct() { 30 | let builder = Struct::builder(); 31 | 32 | let options = builder.get_options(); 33 | let choices: Vec<_> = options.choices.iter().map(|c| c.text.as_str()).collect(); 34 | assert!(choices.contains(&"Edit renamed field")); 35 | assert!(choices.contains(&"Edit enum")); 36 | 37 | let nodes = builder.to_node(); 38 | match nodes { 39 | Node::Leaf(_) => panic!("Expecting a composite"), 40 | Node::Composite(name, fields) => { 41 | assert_eq!(name, "Renamed struct"); 42 | match &fields[0] { 43 | FieldKind::Named(name, _) => { 44 | assert_eq!(name, "renamed field"); 45 | } 46 | FieldKind::Unnamed(_) => panic!("Expecting a named field"), 47 | } 48 | match &fields[1] { 49 | FieldKind::Named(name, _) => { 50 | assert_eq!(name, "enum"); 51 | } 52 | FieldKind::Unnamed(_) => panic!("Expecting a named field"), 53 | } 54 | } 55 | } 56 | } 57 | 58 | #[test] 59 | fn test_enum_options() { 60 | let builder = Enum::builder(); 61 | let options = builder.get_options(); 62 | let choices: Vec<_> = options.choices.iter().map(|c| c.text.as_str()).collect(); 63 | assert!(choices.contains(&"renamed variant")); 64 | assert!(choices.contains(&"renamed variant 2")); 65 | assert!(choices.contains(&"renamed variant 3")); 66 | } 67 | 68 | #[test] 69 | fn test_enum_empty() { 70 | let mut builder = Enum::builder(); 71 | 72 | builder.choose(Input::choice("Var1")).unwrap(); 73 | 74 | let nodes = builder.to_node(); 75 | match nodes { 76 | Node::Leaf(field) => match field { 77 | Field::String(name) => assert_eq!(name, "renamed variant"), 78 | Field::Missing => panic!("Expecting a string"), 79 | }, 80 | Node::Composite(_, _) => panic!("Expecting a leaf"), 81 | } 82 | } 83 | 84 | #[test] 85 | fn test_enum_named() { 86 | let mut builder = Enum::builder(); 87 | 88 | builder.choose(Input::choice("Var2")).unwrap(); 89 | 90 | let options = builder.get_options(); 91 | let mut choices = options.choices.iter().map(|c| c.text.as_str()); 92 | assert!(choices.any(|x| x == "Edit renamed inner field")); 93 | 94 | let nodes = builder.to_node(); 95 | match nodes { 96 | Node::Leaf(_) => panic!("Expecting a composite"), 97 | Node::Composite(name, fields) => { 98 | assert_eq!(name, "renamed variant 2"); 99 | match &fields[0] { 100 | FieldKind::Named(name, _) => { 101 | assert_eq!(name, "renamed inner field"); 102 | } 103 | FieldKind::Unnamed(_) => panic!("Expecting a named"), 104 | } 105 | } 106 | } 107 | } 108 | 109 | #[test] 110 | fn test_enum_unnamed() { 111 | let mut builder = Enum::builder(); 112 | 113 | builder.choose(Input::choice("Var3")).unwrap(); 114 | 115 | let nodes = builder.to_node(); 116 | match nodes { 117 | Node::Leaf(_) => panic!("expecting a composite"), 118 | Node::Composite(name, _) => { 119 | assert_eq!(name, "renamed variant 3"); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ibuilder/tests/interaction.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | use ibuilder::*; 3 | 4 | #[derive(Debug, IBuilder, Eq, PartialEq)] 5 | struct Base { 6 | integer: i32, 7 | #[ibuilder(default = 42)] 8 | defaulted: i32, 9 | inner: Inner, 10 | } 11 | 12 | #[derive(Debug, IBuilder, Eq, PartialEq)] 13 | struct Inner { 14 | string: String, 15 | #[ibuilder(default = "lol")] 16 | defaulted: String, 17 | } 18 | 19 | #[test] 20 | fn test_interaction() { 21 | let mut builder: Builder = Base::builder(); 22 | assert!(!builder.is_done()); 23 | assert!(builder.finalize().is_err()); 24 | 25 | let options = builder.get_options(); 26 | assert!(!options.text_input); 27 | assert!(has_choice("integer", &options)); 28 | assert!(has_choice("defaulted", &options)); 29 | assert!(has_choice("inner", &options)); 30 | assert!(!has_choice(BACK_ID, &options)); 31 | assert!(!has_choice(FINALIZE_ID, &options)); 32 | 33 | let res = builder.choose(Input::choice("nope")); 34 | assert_eq!(res, Err(ChooseError::UnexpectedChoice)); 35 | let res = builder.choose(Input::choice(BACK_ID)); 36 | assert_eq!(res, Err(ChooseError::UnexpectedChoice)); 37 | let res = builder.choose(Input::choice("integer")); 38 | assert_eq!(res, Ok(None)); 39 | 40 | let options = builder.get_options(); 41 | assert!(options.text_input); 42 | assert!(has_choice(BACK_ID, &options)); 43 | 44 | let res = builder.choose(Input::text("nope")); 45 | if let Err(ChooseError::InvalidText { .. }) = res { 46 | } else { 47 | panic!("expecting ChooseError::InvalidText"); 48 | } 49 | let res = builder.choose(Input::text("123")); 50 | assert_eq!(res, Ok(None)); 51 | 52 | let options = builder.get_options(); 53 | assert!(!options.text_input); 54 | assert!(has_choice("integer", &options)); 55 | assert!(has_choice("defaulted", &options)); 56 | assert!(has_choice("inner", &options)); 57 | assert!(!has_choice(BACK_ID, &options)); 58 | assert!(!has_choice(FINALIZE_ID, &options)); 59 | 60 | let res = builder.choose(Input::choice("inner")); 61 | assert_eq!(res, Ok(None)); 62 | 63 | let options = builder.get_options(); 64 | assert!(!options.text_input); 65 | assert!(has_choice("string", &options)); 66 | assert!(has_choice("defaulted", &options)); 67 | assert!(has_choice(BACK_ID, &options)); 68 | assert!(!has_choice(FINALIZE_ID, &options)); 69 | 70 | let res = builder.choose(Input::choice("string")); 71 | assert_eq!(res, Ok(None)); 72 | 73 | let options = builder.get_options(); 74 | assert!(options.text_input); 75 | assert!(has_choice(BACK_ID, &options)); 76 | assert!(!has_choice(FINALIZE_ID, &options)); 77 | 78 | let res = builder.choose(Input::text("lallabalalla")); 79 | assert_eq!(res, Ok(None)); 80 | 81 | let options = builder.get_options(); 82 | assert!(!options.text_input); 83 | assert!(has_choice("string", &options)); 84 | assert!(has_choice("defaulted", &options)); 85 | assert!(has_choice(BACK_ID, &options)); 86 | assert!(!has_choice(FINALIZE_ID, &options)); 87 | 88 | let res = builder.choose(Input::Choice(BACK_ID.into())); 89 | assert_eq!(res, Ok(None)); 90 | 91 | let options = builder.get_options(); 92 | assert!(!options.text_input); 93 | assert!(has_choice("integer", &options)); 94 | assert!(has_choice("defaulted", &options)); 95 | assert!(has_choice("inner", &options)); 96 | assert!(!has_choice(BACK_ID, &options)); 97 | assert!(has_choice(FINALIZE_ID, &options)); 98 | 99 | let res = builder.choose(Input::Choice(FINALIZE_ID.into())); 100 | assert_eq!( 101 | res, 102 | Ok(Some(Base { 103 | integer: 123, 104 | defaulted: 42, 105 | inner: Inner { 106 | string: "lallabalalla".to_string(), 107 | defaulted: "lol".to_string() 108 | } 109 | })) 110 | ); 111 | } 112 | 113 | fn has_choice>(id: S, options: &Options) -> bool { 114 | for opt in &options.choices { 115 | if opt.choice_id == id.as_ref() { 116 | return true; 117 | } 118 | } 119 | false 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ibuilder 2 | 3 | [![Rust](https://github.com/edomora97/ibuilder/workflows/Rust/badge.svg?branch=master)](https://github.com/edomora97/ibuilder/actions?query=workflow%3ARust) 4 | [![Audit](https://github.com/edomora97/ibuilder/workflows/Audit/badge.svg?branch=master)](https://github.com/edomora97/ibuilder/actions?query=workflow%3AAudit) 5 | [![crates.io](https://img.shields.io/crates/v/ibuilder.svg)](https://crates.io/crates/ibuilder) 6 | [![Docs](https://docs.rs/ibuilder/badge.svg)](https://docs.rs/ibuilder) 7 | 8 | Interactive builders for structs. 9 | 10 | This crate provides a way to construct structs interactively, prompting the user multiple 11 | choices and text inputs. 12 | 13 | The builder provides the user with interactive menu-like interfaces, keeping the UI abstract 14 | and rust type-safeness. 15 | 16 | ### Rationale 17 | When building an interactive application (e.g. a Telegram bot or a console application) it can 18 | be pretty cumbersome to come out with a decent interface without writing tons of code for the 19 | logic for handling the parsing and the validation of the input. 20 | 21 | This crates provides a useful abstraction that allows an easy connection between the data and 22 | the user interface. Just by deriving the struct (or enum) that defines your data you can get a 23 | safe interface for building a UI. 24 | 25 | The derive API is inspired by the great [`structopt`](https://docs.rs/structopt) crate. 26 | 27 | ### API Overview 28 | 29 | The API of this crate is very simple: 30 | - Derive a struct (or an enum) from `IBuilder`, including all the structs/enums that it depends 31 | upon; 32 | - Call the `builder()` method (from the `Buildable` trait) to get an instance of `Builder`; 33 | - Call `get_options()` on the builder to get an object that contains a message to show the user, 34 | a list of possible _choices_ (i.e. buttons to press) and eventually the possibility to enter 35 | some text (i.e. a text box); 36 | - Call `to_node()` on the builder to get a tree-like structure with the state of the builder, 37 | highlighting the fields that still need actions; 38 | - You choose how to show to the user the options and when the user made the decision call 39 | `choose(input)` on the builder. This will apply the choice to the state of the structure if 40 | it's valid, or return an error; 41 | - When the state is complete (all the required fields are present) a new option is present in 42 | the list: _Done_. If the user selects it `choose` will return an instance of `T`. 43 | 44 | A list of all the possible options for the `ibuilder` attribute can be found [here](https://docs.rs/ibuilder/*/ibuilder/derive.IBuilder.html). 45 | 46 | ### Supported Features 47 | - Deriving any struct with named fields (or with one unnamed field like `struct Foo(i64)`) 48 | - Enums (also with variants with field, but only one if unnamed) 49 | - Default values for the fields and default variant for enums 50 | - Custom message prompt for fields, structs, enums and variants 51 | - Renaming fields, structs and variants for better looking options 52 | - Hidden fields (that takes the value only from the default) 53 | - Nested structures (i.e. custom types) 54 | - Supported field types: all numeric types from rust, `bool`, `String`, `char`, `Box`, 55 | `Vec` and `Option` 56 | - Any field type that implementes the `NewBuildableValue` trait 57 | 58 | ### Example of Usage 59 | 60 | In this example the data is stored inside a struct named `Person` which has 3 fields, one of 61 | which has a default value. Deriving from `IBuilder` gives access to the `builder()` method that 62 | returns a `Builder`. 63 | 64 | ![Figure 1](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example1.png) | ![Figure 2](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example2.png) | ![Figure 3](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example3.png) 65 | :-------------------------:|:-------------------------:|:-------------------------: 66 | **Figure 1**: main menu | **Figure 2**: `AgeRange` menu | **Figure 3**: main menu again 67 | 68 | ```rust 69 | use ibuilder::*; 70 | 71 | #[derive(IBuilder)] 72 | pub struct Person { 73 | #[ibuilder(rename = "full name")] 74 | full_name: String, 75 | age: AgeRange, 76 | #[ibuilder(default = 2, rename = "number of hands")] 77 | num_hands: u64, 78 | } 79 | 80 | #[derive(IBuilder, Debug, Eq, PartialEq)] 81 | #[ibuilder(prompt = "How old are you?")] 82 | pub enum AgeRange { 83 | #[ibuilder(rename = "Less than 13 years old")] 84 | Child, 85 | #[ibuilder(rename = "From 13 to 19 years old")] 86 | Teen, 87 | #[ibuilder(rename = "20 years or more")] 88 | Adult, 89 | #[ibuilder(rename = "I don't want to tell")] 90 | Unknown, 91 | } 92 | 93 | let mut builder = Person::builder(); 94 | 95 | // * figure 1 * 96 | let options = builder.get_options(); // main menu: select the field to edit 97 | builder.choose(Input::choice("age")).unwrap(); // select the field 98 | 99 | // * figure 2 * 100 | let options = builder.get_options(); // age menu 101 | builder.choose(Input::choice("Adult")).unwrap(); // insert the value 102 | 103 | let options = builder.get_options(); // back to the main menu 104 | builder.choose(Input::choice("full_name")).unwrap(); // select the field 105 | 106 | let options = builder.get_options(); // full_name menu 107 | assert!(options.text_input); // for inserting the string value 108 | builder.choose(Input::text("edomora97")).unwrap(); // insert the value 109 | 110 | // * figure 3 * 111 | assert!(builder.is_done()); 112 | let options = builder.get_options(); // main menu again, but the "Done" option is available 113 | // chose the "Done" option, the return value is Ok(Some(Person)) 114 | let value = builder.choose(Input::Choice(FINALIZE_ID.to_string())).unwrap().unwrap(); 115 | 116 | assert_eq!(value.full_name, "edomora97"); 117 | assert_eq!(value.age, AgeRange::Adult); 118 | assert_eq!(value.num_hands, 2); 119 | ``` 120 | 121 | License: MIT 122 | -------------------------------------------------------------------------------- /ibuilder_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [![crates.io](https://img.shields.io/crates/v/ibuilder_derive.svg)](https://crates.io/crates/ibuilder_derive) 2 | //! [![Docs](https://docs.rs/ibuilder_derive/badge.svg)](https://docs.rs/ibuilder_derive) 3 | //! 4 | //! See the documentation of the [`ibuilder`](https://crates.io/crates/ibuilder) create for the details, 5 | //! you probably are looking for that. 6 | //! 7 | //! ## ibuilder derive macro 8 | //! 9 | //! Usage: 10 | //! ``` 11 | //! # use ibuilder_derive::IBuilder; 12 | //! #[derive(IBuilder)] 13 | //! struct Example { 14 | //! field1: i64, 15 | //! #[ibuilder(default = "something")] 16 | //! field2: String, 17 | //! } 18 | //! ``` 19 | //! 20 | //! Will implement the trait `ibuilder::Buildable` for `Example`, prodiding the `builder()` method 21 | //! for getting a `ibuilder::Builder`. 22 | //! 23 | //! It will also implement a private struct for keeping the state of the builder and implement the 24 | //! `NewBuildableValue` trait for `Example`, allowing it to be inside a fields of other derived 25 | //! types. 26 | 27 | use proc_macro::TokenStream; 28 | 29 | use quote::{quote, ToTokens}; 30 | 31 | use proc_macro_error::{abort, proc_macro_error, set_dummy}; 32 | 33 | use crate::enum_gen::EnumGenerator; 34 | use crate::struct_gen::StructGenerator; 35 | 36 | mod enum_gen; 37 | mod struct_gen; 38 | 39 | /// Derive macro for `IBuilder`. 40 | /// 41 | /// # Supported features 42 | /// This is a somewhat complete list of all the attributes it's possible to use deriving from 43 | /// `IBuilder`. 44 | /// 45 | /// ## `#[ibuilder(rename = "new name")]` 46 | /// When applied to a struct, a named field (of a struct or of a variant) or an enum's variant it 47 | /// changes the named displayed in the list of possible options returned by `get_options()` and in 48 | /// the tree structure returned by `to_node`. 49 | /// 50 | /// Renaming an enum is not is supported since that name is not shown anywhere. 51 | /// 52 | /// ``` 53 | /// # use ibuilder_derive::IBuilder; 54 | /// #[derive(IBuilder)] 55 | /// #[ibuilder(rename = "new struct name")] 56 | /// struct Struct { 57 | /// #[ibuilder(rename = "new field name")] 58 | /// field1: i64, 59 | /// } 60 | /// #[derive(IBuilder)] 61 | /// enum Enum { 62 | /// #[ibuilder(rename = "new variant name")] 63 | /// Var1, 64 | /// Var2 { 65 | /// #[ibuilder(rename = "new field name")] 66 | /// field: i32, 67 | /// }, 68 | /// } 69 | /// ``` 70 | /// 71 | /// ## `#[ibuilder(prompt = "new prompt message")]` 72 | /// Change the message attached to the result of `get_options()` for a struct, an enum, a field or a 73 | /// variant. The prompt set on fields and variants overwrites the one on the structs and enum. If 74 | /// not specified a default value is used. 75 | /// 76 | /// ``` 77 | /// # use ibuilder_derive::IBuilder; 78 | /// #[derive(IBuilder)] 79 | /// #[ibuilder(prompt = "new struct prompt")] 80 | /// struct Struct { 81 | /// #[ibuilder(rename = "new field prompt")] 82 | /// field1: i64, 83 | /// } 84 | /// #[derive(IBuilder)] 85 | /// #[ibuilder(prompt = "new enum prompt")] 86 | /// enum Enum { 87 | /// #[ibuilder(rename = "new variant promp")] 88 | /// Var1, 89 | /// Var2 { 90 | /// #[ibuilder(rename = "new field prompt")] 91 | /// field: i32, 92 | /// }, 93 | /// } 94 | /// ``` 95 | /// 96 | /// ## `#[ibuilder(default = something)]` 97 | /// Set a default value for the field. After the equal sign a literal is expected, if it is a string 98 | /// literal the conversion is done using `FromStr` **at runtime**, otherwise the literal is 99 | /// converted using the `as` syntax. 100 | /// 101 | /// For now only the builtin types can be defaulted (numeric types, bool, char and String). 102 | /// 103 | /// ``` 104 | /// # use ibuilder_derive::IBuilder; 105 | /// #[derive(IBuilder)] 106 | /// struct Struct { 107 | /// #[ibuilder(default = 42)] 108 | /// field1: u8, 109 | /// #[ibuilder(default = "something")] 110 | /// field2: String, 111 | /// #[ibuilder(default = true)] 112 | /// field3: bool, 113 | /// #[ibuilder(default = 'x')] 114 | /// field4: char, 115 | /// } 116 | /// #[derive(IBuilder)] 117 | /// enum Enum { 118 | /// Var { 119 | /// #[ibuilder(default = 42)] 120 | /// field: i32, 121 | /// }, 122 | /// } 123 | /// ``` 124 | /// 125 | /// ## `#[ibuilder(default)]` 126 | /// Set a variant of an enum as the default one for that enum. At most one variant can be set as 127 | /// default. 128 | /// 129 | /// ``` 130 | /// # use ibuilder_derive::IBuilder; 131 | /// #[derive(IBuilder)] 132 | /// enum Enum { 133 | /// #[ibuilder(default)] 134 | /// Var1, 135 | /// Var2, 136 | /// } 137 | /// ``` 138 | /// 139 | /// ## `#[ibuilder(hidden)]` 140 | /// Hide a field or a variant from the return value of `get_options()` and `to_node()`. The field 141 | /// cannot be accessed neither using `apply`. If a field is hidden it must have a default value or 142 | /// it must implement `Default`. 143 | /// 144 | /// When hiding the fields of an enum, at least one of them must be visible. 145 | /// 146 | /// ``` 147 | /// # use ibuilder_derive::IBuilder; 148 | /// #[derive(IBuilder)] 149 | /// struct Struct { 150 | /// #[ibuilder(hidden, default = 42)] 151 | /// field1: u8, 152 | /// #[ibuilder(hidden)] // uses Default::default() 153 | /// field2: u8, 154 | /// } 155 | /// #[derive(IBuilder)] 156 | /// enum Enum { 157 | /// Var1, 158 | /// #[ibuilder(hidden)] 159 | /// Var2, 160 | /// } 161 | /// ``` 162 | #[proc_macro_error] 163 | #[proc_macro_derive(IBuilder, attributes(ibuilder))] 164 | pub fn ibuilder_derive(input: TokenStream) -> TokenStream { 165 | ibuilder_macro(&syn::parse(input).unwrap()) 166 | } 167 | 168 | /// Main macro body. It generates the following constructs: 169 | /// - `impl Buildable for Name` 170 | /// - `impl NewBuildableValue for Name` 171 | /// - Some private structure and enums for keeping the state of the builder for `Name`, all those 172 | /// types have the name starting with `__`. 173 | fn ibuilder_macro(ast: &syn::DeriveInput) -> TokenStream { 174 | fix_double_error(&ast.ident); 175 | match &ast.data { 176 | syn::Data::Struct(_) => StructGenerator::from_struct(ast).to_token_stream().into(), 177 | syn::Data::Enum(_) => EnumGenerator::from_enum(ast).to_token_stream().into(), 178 | _ => abort!(ast, "only structs can derive ibuilder"), 179 | } 180 | } 181 | 182 | /// Parse the string attribute into the `Option`. In case of duplicate or not string a 183 | /// compile error is raised. 184 | fn parse_string_meta(out: &mut Option, lit: syn::Lit) { 185 | if out.is_none() { 186 | match lit { 187 | syn::Lit::Str(content) => *out = Some(content.value()), 188 | _ => abort!(lit, "expecting a string"), 189 | } 190 | } else { 191 | abort!(lit, "duplicated attribute"); 192 | } 193 | } 194 | 195 | fn fix_double_error(ident: &syn::Ident) { 196 | set_dummy(quote! { 197 | impl ibuilder::Buildable<#ident> for #ident { 198 | fn builder() -> ibuilder::Builder<#ident> { unimplemented!() } 199 | } 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /ibuilder_derive/src/struct_gen/named_fields.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use proc_macro_error::abort; 3 | use syn::Ident; 4 | 5 | use quote::{quote, TokenStreamExt}; 6 | 7 | use crate::struct_gen::StructGenerator; 8 | 9 | /// The generator of the implementation of `BuildableValue` for a struct with named fields. 10 | pub struct StructWithNamedFields<'s> { 11 | /// The base struct generator. 12 | gen: &'s StructGenerator, 13 | /// The list of the names of the fields of the original structure. 14 | fields: Vec, 15 | } 16 | 17 | impl<'s> StructWithNamedFields<'s> { 18 | /// Make a new `StructWithNamedFields` from a `StructGenerator`. 19 | pub fn new(gen: &'s StructGenerator) -> Self { 20 | if gen.fields.is_empty() { 21 | abort!(gen.span, "the struct must have at least one field"); 22 | } 23 | Self { 24 | fields: gen 25 | .fields 26 | .iter() 27 | .filter(|f| !f.metadata.hidden) 28 | .filter_map(|f| f.ident.clone()) 29 | .collect(), 30 | gen, 31 | } 32 | } 33 | 34 | /// Generate the implementation of the trait methods. 35 | pub fn gen(&self) -> TokenStream { 36 | let fn_apply = self.gen_fn_apply(); 37 | let fn_get_options = self.gen_fn_get_options(); 38 | let fn_get_subfields = self.gen_fn_get_subfields(); 39 | let fn_to_node = self.gen_fn_to_node(); 40 | let fn_get_value_any = self.gen_fn_get_value_any(); 41 | quote! { 42 | #fn_apply 43 | #fn_get_options 44 | #fn_get_subfields 45 | #fn_to_node 46 | #fn_get_value_any 47 | } 48 | } 49 | 50 | /// Generate the implementation of the `apply` method. 51 | fn gen_fn_apply(&self) -> TokenStream { 52 | let field_names = &self.fields; 53 | quote! { 54 | fn apply(&mut self, data: ibuilder::Input, current_fields: &[String]) -> Result<(), ibuilder::ChooseError> { 55 | if current_fields.is_empty() { 56 | match data { 57 | ibuilder::Input::Choice(data) => { 58 | match data.as_str() { 59 | #(stringify!(#field_names) => {},)* 60 | _ => return Err(ibuilder::ChooseError::UnexpectedChoice), 61 | } 62 | } 63 | _ => return Err(ibuilder::ChooseError::UnexpectedText) 64 | } 65 | } else { 66 | let field = ¤t_fields[0]; 67 | let rest = ¤t_fields[1..]; 68 | match field.as_str() { 69 | #(stringify!(#field_names) => self.#field_names.apply(data, rest)?,)* 70 | _ => Err(ibuilder::ChooseError::UnexpectedChoice)?, 71 | } 72 | } 73 | Ok(()) 74 | } 75 | } 76 | } 77 | 78 | /// Generate the implementation of the `get_options` method. 79 | fn gen_fn_get_options(&self) -> TokenStream { 80 | let field_names = &self.fields; 81 | let choices = self 82 | .gen 83 | .fields 84 | .iter() 85 | .filter(|f| !f.metadata.hidden) 86 | .map(|f| { 87 | let ident = f.ident.as_ref().unwrap(); 88 | let name = f.actual_name(); 89 | quote! { 90 | ibuilder::Choice { 91 | choice_id: stringify!(#ident).to_string(), 92 | text: "Edit ".to_string() + #name, 93 | needs_action: self.#ident.get_value_any().is_none(), 94 | } 95 | } 96 | }); 97 | quote! { 98 | fn get_options(&self, current_fields: &[String]) -> ibuilder::Options { 99 | if current_fields.is_empty() { 100 | ibuilder::Options { 101 | query: self.__prompt.clone(), 102 | text_input: false, 103 | choices: vec![ #(#choices),* ], 104 | } 105 | } else { 106 | let field = ¤t_fields[0]; 107 | let rest = ¤t_fields[1..]; 108 | match field.as_str() { 109 | #(stringify!(#field_names) => self.#field_names.get_options(rest),)* 110 | _ => unreachable!("Invalid current field: {} (the rest is {:?})", field, rest), 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | /// Generate the implementation of the `get_subfields` method. 118 | fn gen_fn_get_subfields(&self) -> TokenStream { 119 | let field_names = &self.fields; 120 | quote! { 121 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 122 | if current_fields.is_empty() { 123 | vec![ #(stringify!(#field_names).to_string(),)* ] 124 | } else { 125 | let field = ¤t_fields[0]; 126 | let rest = ¤t_fields[1..]; 127 | match field.as_str() { 128 | #(stringify!(#field_names) => self.#field_names.get_subfields(rest),)* 129 | _ => unreachable!("Invalid current field: {} (the rest is {:?})", field, rest), 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | /// Generate the implementation of the `to_node` method. 137 | fn gen_fn_to_node(&self) -> TokenStream { 138 | let ident = &self.gen.ident; 139 | let fields: Vec<_> = self 140 | .gen 141 | .fields 142 | .iter() 143 | .filter(|f| !f.metadata.hidden) 144 | .map(|f| { 145 | let ident = f.ident.as_ref().unwrap(); 146 | let name = f.actual_name(); 147 | quote! { 148 | ibuilder::nodes::FieldKind::Named(#name.into(), self.#ident.to_node()) 149 | } 150 | }) 151 | .collect(); 152 | let name = if let Some(name) = &self.gen.metadata.rename { 153 | quote! { #name } 154 | } else { 155 | quote! { stringify!(#ident) } 156 | }; 157 | quote! { 158 | fn to_node(&self) -> ibuilder::nodes::Node { 159 | ibuilder::nodes::Node::Composite( 160 | #name.into(), 161 | vec![ #(#fields,)* ] 162 | ) 163 | } 164 | } 165 | } 166 | 167 | /// Generate the implementation of the `get_value_any` method. 168 | fn gen_fn_get_value_any(&self) -> TokenStream { 169 | let ident = &self.gen.ident; 170 | let mut field_list = TokenStream::new(); 171 | for field in self.gen.fields.iter() { 172 | let field_name = field.ident.as_ref().unwrap(); 173 | field_list.append_all(if field.metadata.hidden { 174 | quote! { #field_name: self.#field_name.clone(), } 175 | } else { 176 | quote! { #field_name: *self.#field_name.get_value_any()?.downcast().unwrap(), } 177 | }); 178 | } 179 | quote! { 180 | fn get_value_any(&self) -> Option> { 181 | Some(Box::new(#ident { 182 | #field_list 183 | })) 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /ibuilder_derive/src/enum_gen/enum_buildable_value_gen.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::enum_gen::{ 5 | gen_variants_builder_ident, gen_variants_builder_variant_ident, EnumGenerator, VariantKind, 6 | }; 7 | 8 | /// Generate the implementation of the `BuildableValue` trait. 9 | pub fn gen_impl_buildable_value(gen: &EnumGenerator) -> TokenStream { 10 | let builder_ident = &gen.builder_ident; 11 | let fn_apply = gen_fn_apply(gen); 12 | let fn_get_options = gen_fn_get_options(gen); 13 | let fn_get_subfields = gen_fn_get_subfields(gen); 14 | let fn_to_node = gen_fn_to_node(gen); 15 | let fn_get_value_any = gen_fn_get_value_any(gen); 16 | quote! { 17 | #[automatically_derived] 18 | #[allow(unreachable_code)] 19 | impl ibuilder::BuildableValue for #builder_ident { 20 | #fn_apply 21 | #fn_get_options 22 | #fn_get_subfields 23 | #fn_to_node 24 | #fn_get_value_any 25 | } 26 | } 27 | } 28 | 29 | /// Generate the implementation of the `apply` method. 30 | /// 31 | /// If the builder is in the variant menu, apply selects the variant to use. If it is already inside 32 | /// a variant it forward the apply to it. 33 | fn gen_fn_apply(gen: &EnumGenerator) -> TokenStream { 34 | let select_menu = fn_apply_select_menu(gen); 35 | let inner_menu = fn_apply_inner_menu(gen); 36 | quote! { 37 | fn apply(&mut self, data: ibuilder::Input, current_fields: &[String]) -> Result<(), ibuilder::ChooseError> { 38 | // select variant menu 39 | if current_fields.is_empty() { 40 | #select_menu 41 | } else { 42 | #inner_menu 43 | } 44 | Ok(()) 45 | } 46 | } 47 | } 48 | 49 | /// Generate the selection of the variant to use. 50 | fn fn_apply_select_menu(gen: &EnumGenerator) -> TokenStream { 51 | let builder = gen_variants_builder_ident(&gen.ident); 52 | let select_menu: Vec<_> = gen 53 | .variants 54 | .iter() 55 | .filter(|v| !v.metadata.hidden) 56 | .map(|var| { 57 | let ident = &var.ident; 58 | let variant_builder_new = var.builder_new(&gen.ident); 59 | let content = if var.kind.is_empty() { 60 | quote! {} 61 | } else { 62 | quote! {(_)} 63 | }; 64 | quote! { 65 | stringify!(#ident) => { 66 | match &self.value { 67 | // do not overwrite if already selected 68 | Some(#builder::#ident #content) => {}, 69 | _ => self.value = Some(#variant_builder_new) 70 | } 71 | } 72 | } 73 | }) 74 | .collect(); 75 | quote! { 76 | match data { 77 | ibuilder::Input::Choice(data) => { 78 | match data.as_str() { 79 | #(#select_menu,)* 80 | _ => return Err(ibuilder::ChooseError::UnexpectedChoice), 81 | } 82 | } 83 | _ => return Err(ibuilder::ChooseError::UnexpectedText) 84 | } 85 | } 86 | } 87 | 88 | /// Generate the forwarding of the apply to the variant. 89 | fn fn_apply_inner_menu(gen: &EnumGenerator) -> TokenStream { 90 | let builder = gen_variants_builder_ident(&gen.ident); 91 | let apply: Vec<_> = gen 92 | .variants 93 | .iter() 94 | .filter(|v| !v.metadata.hidden) 95 | .filter_map(|var| match &var.kind { 96 | VariantKind::Empty => None, 97 | VariantKind::Unnamed(_) | VariantKind::Named(_) => { 98 | let variant = &var.ident; 99 | Some(quote! { 100 | stringify!(#variant) => match self.value.as_mut().unwrap() { 101 | #builder::#variant(inner) => inner.apply(data, rest)?, 102 | _ => unreachable!("Invalid variant in value"), 103 | } 104 | }) 105 | } 106 | }) 107 | .collect(); 108 | quote! { 109 | let field = ¤t_fields[0]; 110 | let rest = ¤t_fields[1..]; 111 | match field.as_str() { 112 | #(#apply,)* 113 | _ => unreachable!("Invalid variant: {}", field), 114 | } 115 | } 116 | } 117 | 118 | /// Generate the implementation of the `get_options` method. 119 | /// 120 | /// If the builder is in the main menu, allow the selection of one of the variants. If already 121 | /// inside a variant forwards the call to it. 122 | fn gen_fn_get_options(gen: &EnumGenerator) -> TokenStream { 123 | let select_menu = fn_get_options_select_menu(gen); 124 | let inner_menu = fn_get_options_inner_menu(gen); 125 | quote! { 126 | fn get_options(&self, current_fields: &[String]) -> ibuilder::Options { 127 | if current_fields.is_empty() { 128 | #select_menu 129 | } else { 130 | #inner_menu 131 | } 132 | } 133 | } 134 | } 135 | 136 | /// Generate the menu of selection of the variant. 137 | fn fn_get_options_select_menu(gen: &EnumGenerator) -> TokenStream { 138 | let builder = gen_variants_builder_ident(&gen.ident); 139 | let choices: Vec<_> = gen 140 | .variants 141 | .iter() 142 | .filter(|v| !v.metadata.hidden) 143 | .map(|var| { 144 | let ident = &var.ident; 145 | let name = var.actual_name(); 146 | let needs_action = match &var.kind { 147 | // empty variants never need actions 148 | VariantKind::Empty => quote! { false }, 149 | VariantKind::Unnamed(_) | VariantKind::Named(_) => { 150 | quote! { 151 | match self.value.as_ref() { 152 | Some(#builder::#ident(inner)) => inner.get_value_any().is_none(), 153 | _ => false, 154 | } 155 | } 156 | } 157 | }; 158 | quote! { 159 | ibuilder::Choice { 160 | choice_id: stringify!(#ident).to_string(), 161 | text: #name.to_string(), 162 | needs_action: #needs_action, 163 | } 164 | } 165 | }) 166 | .collect(); 167 | quote! { 168 | ibuilder::Options { 169 | query: self.prompt.clone(), 170 | text_input: false, 171 | choices: vec![ #(#choices,)* ], 172 | } 173 | } 174 | } 175 | 176 | /// Generate the forwarding of the call to get_options to the variant. 177 | fn fn_get_options_inner_menu(gen: &EnumGenerator) -> TokenStream { 178 | let builder = gen_variants_builder_ident(&gen.ident); 179 | let variants: Vec<_> = gen 180 | .variants 181 | .iter() 182 | .filter(|v| !v.metadata.hidden) 183 | .filter_map(|var| { 184 | let ident = &var.ident; 185 | match &var.kind { 186 | VariantKind::Empty => None, 187 | VariantKind::Unnamed(_) | VariantKind::Named(_) => Some(quote! { 188 | stringify!(#ident) => match self.value.as_ref().unwrap() { 189 | #builder::#ident(inner) => inner.get_options(rest), 190 | _ => unreachable!("Invalid variant in value"), 191 | } 192 | }), 193 | } 194 | }) 195 | .collect(); 196 | quote! { 197 | let field = ¤t_fields[0]; 198 | let rest = ¤t_fields[1..]; 199 | match field.as_str() { 200 | #(#variants,)* 201 | _ => unreachable!("Invalid variant {}", field), 202 | } 203 | } 204 | } 205 | 206 | /// Generate the implementation of the `get_subfields` method. 207 | fn gen_fn_get_subfields(gen: &EnumGenerator) -> TokenStream { 208 | let builder = gen_variants_builder_ident(&gen.ident); 209 | let variants: Vec<_> = gen 210 | .variants 211 | .iter() 212 | .filter(|var| !var.kind.is_empty() && !var.metadata.hidden) 213 | .map(|var| &var.ident) 214 | .collect(); 215 | quote! { 216 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 217 | if current_fields.is_empty() { 218 | vec![ #(stringify!(#variants).to_string(),)* ] 219 | } else { 220 | let field = ¤t_fields[0]; 221 | let rest = ¤t_fields[1..]; 222 | match field.as_str() { 223 | #( 224 | stringify!(#variants) => match self.value.as_ref().unwrap() { 225 | #builder::#variants(inner) => inner.get_subfields(rest), 226 | _ => unreachable!("Invalid variant in value"), 227 | }, 228 | )* 229 | _ => unreachable!("Invalid variant: {}", field), 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | /// Generate the implementation of the `to_node` method. 237 | fn gen_fn_to_node(gen: &EnumGenerator) -> TokenStream { 238 | let builder = gen_variants_builder_ident(&gen.ident); 239 | let variants: Vec<_> = gen 240 | .variants 241 | .iter() 242 | .filter(|v| !v.metadata.hidden) 243 | .map(|var| { 244 | let ident = &var.ident; 245 | let name = var.actual_name(); 246 | match &var.kind { 247 | VariantKind::Empty => quote! { 248 | Some(#builder::#ident) => { 249 | ibuilder::nodes::Node::Leaf(ibuilder::nodes::Field::String(#name.to_string())) 250 | } 251 | }, 252 | VariantKind::Named(_) => quote! { 253 | Some(#builder::#ident(inner)) => { 254 | let inner_node = inner.to_node(); 255 | let fields = match inner_node { 256 | ibuilder::nodes::Node::Composite(_, fields) => fields, 257 | _ => unreachable!("Invalid node of enum content"), 258 | }; 259 | ibuilder::nodes::Node::Composite(#name.to_string(), fields) 260 | } 261 | }, 262 | VariantKind::Unnamed(_) => quote! { 263 | Some(#builder::#ident(inner)) => inner.to_node() 264 | }, 265 | } 266 | }) 267 | .collect(); 268 | quote! { 269 | fn to_node(&self) -> ibuilder::nodes::Node { 270 | match &self.value { 271 | None => ibuilder::nodes::Node::Leaf(ibuilder::nodes::Field::Missing), 272 | #(#variants,)* 273 | _ => unreachable!("Selected an hidden value") 274 | } 275 | } 276 | } 277 | } 278 | 279 | /// Generate the implementation of the `get_value_any` method. 280 | fn gen_fn_get_value_any(gen: &EnumGenerator) -> TokenStream { 281 | let builder = gen_variants_builder_ident(&gen.ident); 282 | let base = &gen.ident; 283 | let variants: Vec<_> = gen 284 | .variants 285 | .iter() 286 | .map(|var| { 287 | let ident = &var.ident; 288 | match &var.kind { 289 | VariantKind::Empty => quote! { #builder::#ident => Box::new(#base::#ident) }, 290 | VariantKind::Named(_) => { 291 | let fields = var.field_names(); 292 | let field_builder = gen_variants_builder_variant_ident(&gen.ident, ident); 293 | quote! { 294 | #builder::#ident(inner) => { 295 | let inner = inner 296 | .get_value_any()? 297 | .downcast::<#field_builder>() 298 | .unwrap(); 299 | Box::new(#base::#ident { 300 | #(#fields: inner.#fields,)* 301 | }) 302 | } 303 | } 304 | } 305 | VariantKind::Unnamed(fields) => { 306 | let fields = (0..fields.len()).map(syn::Index::from); 307 | let field_builder = gen_variants_builder_variant_ident(&gen.ident, ident); 308 | quote! { 309 | #builder::#ident(inner) => { 310 | let inner = inner 311 | .get_value_any()? 312 | .downcast::<#field_builder>() 313 | .unwrap(); 314 | Box::new(#base::#ident(#(inner.#fields,)*)) 315 | } 316 | } 317 | } 318 | } 319 | }) 320 | .collect(); 321 | quote! { 322 | fn get_value_any(&self) -> Option> { 323 | let variant = self.value.as_ref()?; 324 | Some(match variant { 325 | #(#variants,)* 326 | }) 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /ibuilder_derive/src/enum_gen/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use proc_macro_error::{abort, emit_warning, ResultExt}; 3 | use syn::punctuated::Punctuated; 4 | use syn::{Fields, Ident, Meta, MetaNameValue, Token, Variant}; 5 | 6 | use quote::{format_ident, quote, ToTokens, TokenStreamExt}; 7 | 8 | use crate::enum_gen::enum_buildable_value_gen::gen_impl_buildable_value; 9 | use crate::parse_string_meta; 10 | use crate::struct_gen::{StructField, StructGenerator}; 11 | 12 | mod enum_buildable_value_gen; 13 | 14 | /// Generator for all the builder-related tokens for an `enum`. It will generate some new structs 15 | /// and enums that implements `BuildableValue`, and will implement some traits for the various types 16 | /// in the game. 17 | #[derive(Debug)] 18 | pub struct EnumGenerator { 19 | /// The `Ident` of the original enum. 20 | ident: Ident, 21 | /// The `Ident` of the struct that implements `BuildableValue` for the enum. 22 | builder_ident: Ident, 23 | /// The `Ident` of the enum that contains the state of the `BuildableValue`. 24 | variants_builder_ident: Ident, 25 | /// The list of the variants of the original enum. 26 | variants: Vec, 27 | /// The metadata associated with the enum. 28 | metadata: EnumMetadata, 29 | } 30 | 31 | /// The metadata of the enum, it's taken from the attributes of the `enum`. 32 | #[derive(Debug)] 33 | pub struct EnumMetadata { 34 | /// The prompt to use for this enum's main menu. 35 | prompt: Option, 36 | } 37 | 38 | /// The information about a variant of an enum. 39 | #[derive(Debug)] 40 | pub struct EnumVariant { 41 | /// The `Ident` of the variant. 42 | ident: Ident, 43 | /// The kind of the variant. 44 | kind: VariantKind, 45 | /// The metadata associated with the variant. 46 | metadata: VariantMetadata, 47 | } 48 | 49 | /// The metadata of the variant, it's taken from the attributes of the `Variant`. 50 | #[derive(Debug)] 51 | pub struct VariantMetadata { 52 | /// The prompt to use for this variant. 53 | prompt: Option, 54 | /// Different name to use in the tree structure. 55 | rename: Option, 56 | /// Whether this variant is hidden. 57 | hidden: bool, 58 | /// Whether this is the default variant. 59 | default: bool, 60 | } 61 | 62 | /// The information about the type of variant. 63 | #[derive(Debug)] 64 | pub enum VariantKind { 65 | /// The variant doesn't contain any field. 66 | Empty, 67 | /// The variant contains only unnamed fields. 68 | Unnamed(Vec), 69 | /// The variant contains only named fields. 70 | Named(Vec), 71 | } 72 | 73 | /// Generator for the list of variant definition of an enum. 74 | struct VariantsDefList<'s> { 75 | /// A reference to the original generator. 76 | gen: &'s EnumGenerator, 77 | } 78 | 79 | impl EnumGenerator { 80 | /// Construct a new `EnumGenerator` from the AST of an enum. Will fail if the AST is not 81 | /// relative to an enum. 82 | pub fn from_enum(ast: &syn::DeriveInput) -> EnumGenerator { 83 | match &ast.data { 84 | syn::Data::Enum(data) => { 85 | let generator = EnumGenerator { 86 | ident: ast.ident.clone(), 87 | builder_ident: gen_builder_ident(&ast.ident), 88 | variants_builder_ident: gen_variants_builder_ident(&ast.ident), 89 | variants: data.variants.iter().map(EnumVariant::from).collect(), 90 | metadata: EnumMetadata::from(ast), 91 | }; 92 | if generator.variants.iter().all(|v| v.metadata.hidden) { 93 | abort!(ast, "all the variants are hidden"); 94 | } 95 | if generator 96 | .variants 97 | .iter() 98 | .filter(|v| v.metadata.default) 99 | .count() 100 | > 1 101 | { 102 | abort!(ast, "at most one variant can be the default"); 103 | } 104 | generator 105 | } 106 | _ => panic!("expecting an enum"), 107 | } 108 | } 109 | 110 | /// Make a new `VariantsDefList` for this enum. 111 | fn variants_def_list(&self) -> VariantsDefList { 112 | VariantsDefList { gen: self } 113 | } 114 | } 115 | 116 | impl EnumVariant { 117 | /// Generate (or not in case of empty variants) a structure that contains the internal state 118 | /// of a variant. This struct will have the same fields as the variant, and derives from 119 | /// `IBuilder`. 120 | fn gen_builder(&self, ident: Ident) -> TokenStream { 121 | let name = self.actual_name(); 122 | let mut attrs = Vec::new(); 123 | if let Some(prompt) = &self.metadata.prompt { 124 | attrs.push(quote! { prompt = #prompt }); 125 | } 126 | attrs.push(quote! { rename = #name }); 127 | let fields_def = match &self.kind { 128 | VariantKind::Empty => return TokenStream::new(), 129 | VariantKind::Unnamed(fields) => { 130 | let fields: Vec<_> = fields.iter().map(|f| &f.field).collect(); 131 | if fields.len() != 1 { 132 | abort!( 133 | self.ident, 134 | "variants with unnamed fields are supported only with one field" 135 | ); 136 | } 137 | quote! { (#(#fields,)*); } 138 | } 139 | VariantKind::Named(fields) => { 140 | let fields: Vec<_> = fields.iter().map(|f| &f.field).collect(); 141 | quote! { { #(#fields,)* } } 142 | } 143 | }; 144 | quote! { 145 | #[allow(non_camel_case_types)] 146 | #[derive(IBuilder)] 147 | #[ibuilder(#(#attrs,)*)] 148 | struct #ident #fields_def 149 | } 150 | } 151 | 152 | /// Return the tokens for initializing the builder of this variant. 153 | fn builder_new(&self, base: &Ident) -> TokenStream { 154 | let variant = &self.ident; 155 | let builder = gen_variants_builder_ident(base); 156 | let variant_builder = gen_variants_builder_variant_ident(base, variant); 157 | let variant_builder = StructGenerator::gen_builder_ident(&variant_builder); 158 | match &self.kind { 159 | VariantKind::Empty => quote! { #builder::#variant }, 160 | VariantKind::Unnamed(_) | VariantKind::Named(_) => { 161 | let prompt = match &self.metadata.prompt { 162 | Some(prompt) => quote! {Some(#prompt.into())}, 163 | None => quote! {None}, 164 | }; 165 | quote! { 166 | #builder::#variant(#variant_builder::new(ibuilder::BuildableValueConfig { 167 | default: None, 168 | prompt: #prompt, 169 | })) 170 | } 171 | } 172 | } 173 | } 174 | 175 | /// Return the list with the names of all the named fields in this variant. 176 | fn field_names(&self) -> Vec { 177 | match &self.kind { 178 | VariantKind::Unnamed(_) | VariantKind::Empty => vec![], 179 | VariantKind::Named(fields) => fields.iter().map(|f| f.ident.clone().unwrap()).collect(), 180 | } 181 | } 182 | 183 | /// Return the actual name of the variant, which is the defined name or the renamed one. The 184 | /// string literal of the name is returned. 185 | fn actual_name(&self) -> TokenStream { 186 | if let Some(renamed) = &self.metadata.rename { 187 | quote! { #renamed } 188 | } else { 189 | let ident = self.ident.to_string(); 190 | quote! { #ident } 191 | } 192 | } 193 | } 194 | 195 | impl From<&syn::DeriveInput> for EnumMetadata { 196 | fn from(data: &syn::DeriveInput) -> EnumMetadata { 197 | let mut metadata = EnumMetadata { prompt: None }; 198 | for attr in &data.attrs { 199 | if attr.path.is_ident("ibuilder") { 200 | let meta = attr 201 | .parse_args_with(Punctuated::::parse_terminated) 202 | .unwrap_or_abort(); 203 | for meta in meta { 204 | parse_enum_meta(meta, &mut metadata); 205 | } 206 | } 207 | } 208 | metadata 209 | } 210 | } 211 | 212 | /// Extract the `EnumMetadata` from a `Meta` entry in an attribute. `meta` comes from 213 | /// `#[ibuilder(HERE)]`. 214 | fn parse_enum_meta(meta: Meta, metadata: &mut EnumMetadata) { 215 | match meta { 216 | Meta::NameValue(MetaNameValue { path, lit, .. }) => { 217 | if path.is_ident("prompt") { 218 | parse_string_meta(&mut metadata.prompt, lit); 219 | } else if path.is_ident("rename") { 220 | abort!( 221 | path, 222 | "renaming an enum is not supported since the name is not exposed" 223 | ); 224 | } else { 225 | abort!(path, "unknown attribute"); 226 | } 227 | } 228 | _ => abort!(meta, "unknown attribute"), 229 | } 230 | } 231 | 232 | impl VariantKind { 233 | /// Check if this is `VariantKind::Empty`. 234 | fn is_empty(&self) -> bool { 235 | matches!(self, VariantKind::Empty) 236 | } 237 | } 238 | 239 | impl From<&Variant> for EnumVariant { 240 | fn from(variant: &Variant) -> EnumVariant { 241 | let metadata = VariantMetadata::from(variant); 242 | EnumVariant { 243 | ident: variant.ident.clone(), 244 | kind: match &variant.fields { 245 | Fields::Named(fields) => { 246 | VariantKind::Named(fields.named.iter().map(StructField::from).collect()) 247 | } 248 | Fields::Unnamed(fields) => { 249 | let mut fields: Vec<_> = fields.unnamed.iter().map(StructField::from).collect(); 250 | // forward the prompt to the unnamed fields to avoid having to add the attribute 251 | // for the field (i.e. inside the parenthesis). 252 | if let Some(prompt) = &metadata.prompt { 253 | for field in fields.iter_mut() { 254 | if field.metadata.prompt.is_none() { 255 | field.metadata.prompt = Some(prompt.clone()); 256 | } 257 | } 258 | } 259 | VariantKind::Unnamed(fields) 260 | } 261 | Fields::Unit => { 262 | if metadata.prompt.is_some() { 263 | abort!(variant, "prompt not supported for empty variants"); 264 | } 265 | VariantKind::Empty 266 | } 267 | }, 268 | metadata, 269 | } 270 | } 271 | } 272 | 273 | impl From<&Variant> for VariantMetadata { 274 | fn from(var: &Variant) -> Self { 275 | let mut metadata = VariantMetadata { 276 | prompt: None, 277 | rename: None, 278 | hidden: false, 279 | default: false, 280 | }; 281 | for attr in &var.attrs { 282 | if attr.path.is_ident("ibuilder") { 283 | let meta = attr 284 | .parse_args_with(Punctuated::::parse_terminated) 285 | .unwrap_or_abort(); 286 | for meta in meta { 287 | parse_variant_meta(meta, &mut metadata); 288 | } 289 | } 290 | } 291 | metadata 292 | } 293 | } 294 | 295 | /// Extract the `VariantMetadata` from a `Meta` entry in a variant attribute. `meta` comes from 296 | /// `#[ibuilder(HERE)]`. 297 | fn parse_variant_meta(meta: Meta, metadata: &mut VariantMetadata) { 298 | match meta { 299 | Meta::NameValue(MetaNameValue { path, lit, .. }) => { 300 | if path.is_ident("prompt") { 301 | parse_string_meta(&mut metadata.prompt, lit); 302 | } else if path.is_ident("rename") { 303 | parse_string_meta(&mut metadata.rename, lit); 304 | } else { 305 | abort!(path, "unknown attribute"); 306 | } 307 | } 308 | Meta::Path(path) => { 309 | if path.is_ident("hidden") { 310 | if metadata.hidden { 311 | emit_warning!(path, "duplicated attribute"); 312 | } 313 | metadata.hidden = true; 314 | } else if path.is_ident("default") { 315 | if metadata.default { 316 | emit_warning!(path, "duplicated attribute"); 317 | } 318 | metadata.default = true; 319 | } else { 320 | abort!(path, "unknown attribute"); 321 | } 322 | } 323 | _ => abort!(meta, "unknown attribute"), 324 | } 325 | } 326 | 327 | /// Generate the `Ident` to use as the implementation of `BuildableValue` for an enum. The type of 328 | /// this element is `struct`. 329 | fn gen_builder_ident(ident: &Ident) -> Ident { 330 | format_ident!("__{}_BuildableValueImpl", ident) 331 | } 332 | 333 | /// Generate the `Ident` to use as the inner state of the `BuildableValue` of the enum. The type 334 | /// of this element is `enum`. 335 | fn gen_variants_builder_ident(ident: &Ident) -> Ident { 336 | format_ident!("__{}_Variants_BuildableValueImpl", ident) 337 | } 338 | 339 | /// Generate the `Ident` to use as the state of a variant with fields. The type of this element is 340 | /// `struct`. 341 | fn gen_variants_builder_variant_ident(ident: &Ident, variant: &Ident) -> Ident { 342 | format_ident!("__{}_Variants_{}", ident, variant) 343 | } 344 | 345 | impl ToTokens for EnumGenerator { 346 | fn to_tokens(&self, tokens: &mut TokenStream) { 347 | tokens.append_all(gen_struct_builder(self)); 348 | tokens.append_all(gen_variants_builder(self)); 349 | // generate the structs for keeping the state of the fields of the variants 350 | for variant in &self.variants { 351 | tokens.append_all(variant.gen_builder(gen_variants_builder_variant_ident( 352 | &self.ident, 353 | &variant.ident, 354 | ))); 355 | } 356 | tokens.append_all(gen_impl_new_buildable_value(self)); 357 | tokens.append_all(gen_impl_buildable_value(self)); 358 | } 359 | } 360 | 361 | impl ToTokens for VariantsDefList<'_> { 362 | fn to_tokens(&self, tokens: &mut TokenStream) { 363 | for variant in &self.gen.variants { 364 | let ident = &variant.ident; 365 | tokens.append_all(quote! {#ident}); 366 | match &variant.kind { 367 | VariantKind::Unnamed(_) | VariantKind::Named(_) => { 368 | let variant_builder = 369 | gen_variants_builder_variant_ident(&self.gen.ident, ident); 370 | let variant_builder = StructGenerator::gen_builder_ident(&variant_builder); 371 | tokens.append_all(quote! { (#variant_builder) }); 372 | } 373 | VariantKind::Empty => {} 374 | } 375 | tokens.append_all(quote! {,}); 376 | } 377 | } 378 | } 379 | 380 | /// Generate the structure that implements `BuildableValue` for the enum, and implement the `new()` 381 | /// function for it. 382 | fn gen_struct_builder(gen: &EnumGenerator) -> TokenStream { 383 | let builder_ident = &gen.builder_ident; 384 | let variants_builder_ident = &gen.variants_builder_ident; 385 | let prompt = if let Some(prompt) = &gen.metadata.prompt { 386 | prompt 387 | } else { 388 | "Select a variant" 389 | }; 390 | let mut default = quote! { None }; 391 | for var in &gen.variants { 392 | if var.metadata.default { 393 | let init = var.builder_new(&gen.ident); 394 | default = quote! { Some(#init) }; 395 | } 396 | } 397 | quote! { 398 | #[automatically_derived] 399 | #[allow(non_camel_case_types)] 400 | #[doc(hidden)] 401 | #[derive(Debug)] 402 | struct #builder_ident { 403 | value: Option<#variants_builder_ident>, 404 | prompt: String, 405 | } 406 | 407 | #[automatically_derived] 408 | impl #builder_ident { 409 | fn new(config: ibuilder::BuildableValueConfig<()>) -> #builder_ident { 410 | #builder_ident { 411 | value: #default, 412 | prompt: config.prompt.unwrap_or_else(|| #prompt.to_string()) 413 | } 414 | } 415 | } 416 | } 417 | } 418 | 419 | /// Generate the enum that contains the internal state of the `BuildableValue` for the enum. 420 | fn gen_variants_builder(gen: &EnumGenerator) -> TokenStream { 421 | let variants_builder_ident = &gen.variants_builder_ident; 422 | let variants = gen.variants_def_list(); 423 | quote! { 424 | #[automatically_derived] 425 | #[allow(non_camel_case_types)] 426 | #[doc(hidden)] 427 | #[derive(Debug)] 428 | enum #variants_builder_ident { 429 | #variants 430 | } 431 | } 432 | } 433 | 434 | /// Generate the implementation of `NewBuildableValue` for the enum. 435 | fn gen_impl_new_buildable_value(gen: &EnumGenerator) -> TokenStream { 436 | let ident = &gen.ident; 437 | let builder_ident = &gen.builder_ident; 438 | quote! { 439 | #[automatically_derived] 440 | impl ibuilder::NewBuildableValue for #ident { 441 | fn new_buildable_value(config: ibuilder::BuildableValueConfig<()>) -> Box { 442 | Box::new(#builder_ident::new(config)) 443 | } 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /ibuilder/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [![Rust](https://github.com/edomora97/ibuilder/workflows/Rust/badge.svg?branch=master)](https://github.com/edomora97/ibuilder/actions?query=workflow%3ARust) 2 | //! [![Audit](https://github.com/edomora97/ibuilder/workflows/Audit/badge.svg?branch=master)](https://github.com/edomora97/ibuilder/actions?query=workflow%3AAudit) 3 | //! [![crates.io](https://img.shields.io/crates/v/ibuilder.svg)](https://crates.io/crates/ibuilder) 4 | //! [![Docs](https://docs.rs/ibuilder/badge.svg)](https://docs.rs/ibuilder) 5 | //! 6 | //! Interactive builders for structs. 7 | //! 8 | //! This crate provides a way to construct structs interactively, prompting the user multiple 9 | //! choices and text inputs. 10 | //! 11 | //! The builder provides the user with interactive menu-like interfaces, keeping the UI abstract 12 | //! and rust type-safeness. 13 | //! 14 | //! ## Rationale 15 | //! When building an interactive application (e.g. a Telegram bot or a console application) it can 16 | //! be pretty cumbersome to come out with a decent interface without writing tons of code for the 17 | //! logic for handling the parsing and the validation of the input. 18 | //! 19 | //! This crates provides a useful abstraction that allows an easy connection between the data and 20 | //! the user interface. Just by deriving the struct (or enum) that defines your data you can get a 21 | //! safe interface for building a UI. 22 | //! 23 | //! The derive API is inspired by the great [`structopt`](https://docs.rs/structopt) crate. 24 | //! 25 | //! ## API Overview 26 | //! 27 | //! The API of this crate is very simple: 28 | //! - Derive a struct (or an enum) from `IBuilder`, including all the structs/enums that it depends 29 | //! upon; 30 | //! - Call the `builder()` method (from the `Buildable` trait) to get an instance of `Builder`; 31 | //! - Call `get_options()` on the builder to get an object that contains a message to show the user, 32 | //! a list of possible _choices_ (i.e. buttons to press) and eventually the possibility to enter 33 | //! some text (i.e. a text box); 34 | //! - Call `to_node()` on the builder to get a tree-like structure with the state of the builder, 35 | //! highlighting the fields that still need actions; 36 | //! - You choose how to show to the user the options and when the user made the decision call 37 | //! `choose(input)` on the builder. This will apply the choice to the state of the structure if 38 | //! it's valid, or return an error; 39 | //! - When the state is complete (all the required fields are present) a new option is present in 40 | //! the list: _Done_. If the user selects it `choose` will return an instance of `T`. 41 | //! 42 | //! A list of all the possible options for the `ibuilder` attribute can be found [here](https://docs.rs/ibuilder/*/ibuilder/derive.IBuilder.html). 43 | //! 44 | //! ## Supported Features 45 | //! - Deriving any struct with named fields (or with one unnamed field like `struct Foo(i64)`) 46 | //! - Enums (also with variants with field, but only one if unnamed) 47 | //! - Default values for the fields and default variant for enums 48 | //! - Custom message prompt for fields, structs, enums and variants 49 | //! - Renaming fields, structs and variants for better looking options 50 | //! - Hidden fields (that takes the value only from the default) 51 | //! - Nested structures (i.e. custom types) 52 | //! - Supported field types: all numeric types from rust, `bool`, `String`, `char`, `Box`, 53 | //! `Vec` and `Option` 54 | //! - Any field type that implementes the `NewBuildableValue` trait 55 | //! 56 | //! ## Example of Usage 57 | //! 58 | //! In this example the data is stored inside a struct named `Person` which has 3 fields, one of 59 | //! which has a default value. Deriving from `IBuilder` gives access to the `builder()` method that 60 | //! returns a `Builder`. 61 | //! 62 | //! ![Figure 1](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example1.png) | ![Figure 2](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example2.png) | ![Figure 3](https://raw.githubusercontent.com/edomora97/ibuilder/8a4ad5d26fb508b8488b26c63fc9e5c80d51467c/docs/example3.png) 63 | //! :-------------------------:|:-------------------------:|:-------------------------: 64 | //! **Figure 1**: main menu | **Figure 2**: `AgeRange` menu | **Figure 3**: main menu again 65 | //! 66 | //! ``` 67 | //! use ibuilder::*; 68 | //! 69 | //! #[derive(IBuilder)] 70 | //! pub struct Person { 71 | //! #[ibuilder(rename = "full name")] 72 | //! full_name: String, 73 | //! age: AgeRange, 74 | //! #[ibuilder(default = 2, rename = "number of hands")] 75 | //! num_hands: u64, 76 | //! } 77 | //! 78 | //! #[derive(IBuilder, Debug, Eq, PartialEq)] 79 | //! #[ibuilder(prompt = "How old are you?")] 80 | //! pub enum AgeRange { 81 | //! #[ibuilder(rename = "Less than 13 years old")] 82 | //! Child, 83 | //! #[ibuilder(rename = "From 13 to 19 years old")] 84 | //! Teen, 85 | //! #[ibuilder(rename = "20 years or more")] 86 | //! Adult, 87 | //! #[ibuilder(rename = "I don't want to tell")] 88 | //! Unknown, 89 | //! } 90 | //! 91 | //! let mut builder = Person::builder(); 92 | //! 93 | //! // * figure 1 * 94 | //! let options = builder.get_options(); // main menu: select the field to edit 95 | //! builder.choose(Input::choice("age")).unwrap(); // select the field 96 | //! 97 | //! // * figure 2 * 98 | //! let options = builder.get_options(); // age menu 99 | //! builder.choose(Input::choice("Adult")).unwrap(); // insert the value 100 | //! 101 | //! let options = builder.get_options(); // back to the main menu 102 | //! builder.choose(Input::choice("full_name")).unwrap(); // select the field 103 | //! 104 | //! let options = builder.get_options(); // full_name menu 105 | //! assert!(options.text_input); // for inserting the string value 106 | //! builder.choose(Input::text("edomora97")).unwrap(); // insert the value 107 | //! 108 | //! // * figure 3 * 109 | //! assert!(builder.is_done()); 110 | //! let options = builder.get_options(); // main menu again, but the "Done" option is available 111 | //! // chose the "Done" option, the return value is Ok(Some(Person)) 112 | //! let value = builder.choose(Input::Choice(FINALIZE_ID.to_string())).unwrap().unwrap(); 113 | //! 114 | //! assert_eq!(value.full_name, "edomora97"); 115 | //! assert_eq!(value.age, AgeRange::Adult); 116 | //! assert_eq!(value.num_hands, 2); 117 | //! ``` 118 | 119 | #[cfg(feature = "derive")] 120 | pub use ibuilder_derive::IBuilder; 121 | 122 | use std::any::Any; 123 | use std::marker::PhantomData; 124 | 125 | use failure::Fail; 126 | 127 | use crate::nodes::Node; 128 | 129 | pub mod builders; 130 | pub mod nodes; 131 | 132 | /// The identifier of the "Done" choice. 133 | pub const FINALIZE_ID: &str = "__finalize"; 134 | /// The identifier of the "Back" choice. 135 | pub const BACK_ID: &str = "__back"; 136 | 137 | /// Interactive builder for creating instances of the struct `T` by communicating. To instantiate a 138 | /// new `Builder` for the type `T`, make `T` derive from `IBuilder` and call `builder()` on it from 139 | /// the `Buildable` trait. 140 | /// 141 | /// ## Communication 142 | /// After having instantiated a new `Builder` you can call the `get_options()` method for 143 | /// fetching the list of possible actions that can be done to update the builder. Those options are 144 | /// like menu entries used to move between menus and set the value of the fields. 145 | /// 146 | /// The `Options` struct contains a list of possible `Choice`s (like buttons to press) and 147 | /// eventually allow raw text input (like a textbox). For example while editing an integer field 148 | /// the user can insert the new value of the number _as a text_ or can choose to go back to the 149 | /// previous menu by pressing on "back". 150 | /// 151 | /// The user's input is communicated to the `Builder` via the `choose` method. It takes an `Input`, 152 | /// a container with the choice of the user, which can be either some `Text` (if the `Options` 153 | /// allowed it), or a `Choice` (whose content is the identifier of the selected option between the 154 | /// ones in the `Options`). 155 | /// 156 | /// When the user has filled all the fields of the builder, he can select the "done" options, which 157 | /// will make the `choose` method return `Ok(Some(T))`, signaling the end of the communication. 158 | #[derive(Debug)] 159 | pub struct Builder { 160 | builder: Box, 161 | current_fields: Vec, 162 | inner_type: PhantomData, 163 | } 164 | 165 | /// A type that supports being built using a `Builder`. Deriving `IBuilder` an auto-generated 166 | /// implementation for this trait is provided. 167 | pub trait Buildable { 168 | /// Create a new `Builder` for the current type. 169 | fn builder() -> Builder; 170 | } 171 | 172 | impl Buildable for T 173 | where 174 | T: NewBuildableValue + 'static, 175 | { 176 | fn builder() -> Builder { 177 | Builder::::from_buildable_value(T::new_buildable_value(Default::default())) 178 | } 179 | } 180 | 181 | /// The interactive builder for a base type. 182 | pub trait BuildableValue: std::fmt::Debug { 183 | /// Try to change the inner value using the provided input. 184 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError>; 185 | 186 | /// The options to show to the user for setting this value. 187 | fn get_options(&self, current_fields: &[String]) -> Options; 188 | 189 | /// Whether this value contains itself other values (i.e. it's a struct). 190 | fn get_subfields(&self, current_fields: &[String]) -> Vec; 191 | 192 | /// Create the tree structure of this value. 193 | fn to_node(&self) -> Node; 194 | 195 | /// Get the inner value, if present, as an `Any`. 196 | /// 197 | /// It's **very important** that the returned `Any` internal type matches the type that this 198 | /// builder is used for. The `Builder` will downcast this `Any` to the types it's expecting, 199 | /// panicking in case of mismatched type. 200 | fn get_value_any(&self) -> Option>; 201 | } 202 | 203 | /// A type that can be built with a `BuildableValue` inside a `Builder`. Keep in mind that the 204 | /// semantics of the generated builder must be compatible with this type, especially looking at the 205 | /// `get_value_any` method. 206 | pub trait NewBuildableValue { 207 | /// Construct a new `BuildableValue` using the provided configuration. Note that using this 208 | /// constructor instead of the `new` method of the actual builder opaques the inner type. 209 | fn new_buildable_value(config: BuildableValueConfig<()>) -> Box; 210 | } 211 | 212 | /// The configuration for customizing the aspect of a `BuildableValue` that produces a value of type 213 | /// `T`. 214 | pub struct BuildableValueConfig { 215 | /// The default value to use, if `None` there is no default value and the field must be 216 | /// provided. 217 | pub default: Option, 218 | /// The prompt message to show to the user, if `None` a default message is shown. 219 | pub prompt: Option, 220 | } 221 | 222 | impl Default for BuildableValueConfig { 223 | fn default() -> Self { 224 | Self { 225 | default: None, 226 | prompt: None, 227 | } 228 | } 229 | } 230 | 231 | impl> Default for Builder { 232 | fn default() -> Self { 233 | T::builder() 234 | } 235 | } 236 | 237 | impl Builder { 238 | /// Create a new builder from a `BuildableValue`. Note that the inner type of the 239 | /// `BuildableValue` must match `T`, otherwise a panic is very likely. 240 | pub fn from_buildable_value(inner: Box) -> Builder { 241 | Self { 242 | builder: inner, 243 | current_fields: vec![], 244 | inner_type: Default::default(), 245 | } 246 | } 247 | 248 | /// Return all the valid options that this builder accepts in the current state. 249 | pub fn get_options(&self) -> Options { 250 | let mut options = self.builder.get_options(&self.current_fields); 251 | // main menu 252 | if self.current_fields.is_empty() { 253 | if self.is_done() { 254 | options.choices.push(Choice { 255 | choice_id: FINALIZE_ID.to_string(), 256 | text: "Done".to_string(), 257 | needs_action: false, 258 | }); 259 | } 260 | // field menu 261 | } else { 262 | options.choices.push(Choice { 263 | choice_id: BACK_ID.to_string(), 264 | text: "Go back".to_string(), 265 | needs_action: false, 266 | }); 267 | } 268 | options 269 | } 270 | 271 | /// Apply an input to the builder, making it change state. Call again `get_options()` for the 272 | /// new options. 273 | /// 274 | /// Returns `Ok(None)` if the process is not done yet, `Ok(Some(T))` when the user choose to 275 | /// finish the builder. 276 | pub fn choose(&mut self, input: Input) -> Result, ChooseError> { 277 | // main menu 278 | if self.current_fields.is_empty() { 279 | if let Input::Choice(data) = &input { 280 | if data == FINALIZE_ID && self.is_done() { 281 | return Ok(Some(self.finalize().expect("Finalize failed"))); 282 | } 283 | } 284 | 285 | // field menu 286 | } else { 287 | match &input { 288 | Input::Choice(data) if data == BACK_ID => { 289 | self.current_fields.pop(); 290 | return Ok(None); 291 | } 292 | _ => {} 293 | } 294 | }; 295 | let subfields = self.builder.get_subfields(&self.current_fields); 296 | for subfield in subfields { 297 | match &input { 298 | Input::Choice(data) => { 299 | if subfield == data.as_str() { 300 | self.builder.apply(input, &self.current_fields)?; 301 | self.current_fields.push(subfield); 302 | return Ok(None); 303 | } 304 | } 305 | Input::Text(_) => {} 306 | } 307 | } 308 | self.builder.apply(input, &self.current_fields)?; 309 | self.current_fields.pop(); 310 | Ok(None) 311 | } 312 | 313 | /// If the process is done try to finalize the process, even if the user hasn't completed the 314 | /// the selection yet. 315 | pub fn finalize(&self) -> Result { 316 | self.builder 317 | .get_value_any() 318 | .ok_or(FinalizeError::MissingField) 319 | .map(|r| *r.downcast::().unwrap()) 320 | } 321 | 322 | /// Check if all the fields have been set and the call to `finalize()` will be successful. 323 | pub fn is_done(&self) -> bool { 324 | self.builder.get_value_any().is_some() 325 | } 326 | 327 | /// Return the tree structure of the `Builder` internal state. 328 | pub fn to_node(&self) -> Node { 329 | self.builder.to_node() 330 | } 331 | } 332 | 333 | /// The options that the user has for the next choice in the `Builder`. 334 | #[derive(Debug, Eq, PartialEq)] 335 | pub struct Options { 336 | /// A textual message with the query to show to the user. 337 | pub query: String, 338 | /// Whether the user can insert raw textual inputs (i.e. `Input::Text`). 339 | pub text_input: bool, 340 | /// The list of all the choices the user can use. 341 | pub choices: Vec, 342 | } 343 | 344 | /// A single choice that the user can select. 345 | #[derive(Debug, Eq, PartialEq)] 346 | pub struct Choice { 347 | /// Identifier of the choice, may not be shown to the user. Its value has to be used as the 348 | /// value in `Input::Choice`. 349 | pub choice_id: String, 350 | /// Textual message to show to the user about this choice. 351 | pub text: String, 352 | /// This choice probably needs to be selected sooner or later because there is a field inside 353 | /// that is missing. 354 | pub needs_action: bool, 355 | } 356 | 357 | /// An input of the user to the `Builder`. 358 | #[derive(Debug, Eq, PartialEq)] 359 | pub enum Input { 360 | /// The user inserted some raw textual content. Can be used only if the `text_input` field of 361 | /// the last `Options` was set to `true`. 362 | Text(String), 363 | /// The user selected one of the multiple choices in the `Options`. The value should be one of 364 | /// the `choice_id` inside the list of `Choice`s of the last `Options`. 365 | Choice(String), 366 | } 367 | 368 | impl Input { 369 | /// The user inserted some raw textual content. Can be used only if the `text_input` field of 370 | /// the last `Options` was set to `true`. 371 | pub fn text>(text: S) -> Input { 372 | Input::Text(text.into()) 373 | } 374 | /// The user selected one of the multiple choices in the `Options`. The value should be one of 375 | /// the `choice_id` inside the list of `Choice`s of the last `Options`. 376 | pub fn choice>(choice: S) -> Input { 377 | Input::Choice(choice.into()) 378 | } 379 | } 380 | 381 | /// The `Input` provided to `Builder::choose` was is invalid. 382 | #[derive(Debug, Fail, Eq, PartialEq)] 383 | pub enum ChooseError { 384 | /// The textual input is not valid. 385 | #[fail(display = "Invalid input: {}", error)] 386 | InvalidText { error: String }, 387 | /// Provided `Input::Text` even though `Options::text_input` was set to `false`. 388 | #[fail(display = "Unexpected text")] 389 | UnexpectedText, 390 | /// Provided an `Input::Choice` with an invalid id. 391 | #[fail(display = "Unexpected choice")] 392 | UnexpectedChoice, 393 | } 394 | 395 | /// The finalization of the result failed. 396 | #[derive(Debug, Fail, Eq, PartialEq)] 397 | pub enum FinalizeError { 398 | /// One or more fields were still missing. 399 | #[fail(display = "There is at least a missing field")] 400 | MissingField, 401 | } 402 | -------------------------------------------------------------------------------- /ibuilder_derive/src/struct_gen/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_error::{abort, emit_warning, ResultExt}; 3 | use syn::punctuated::Punctuated; 4 | use syn::{Field, Fields, Ident, Meta, MetaNameValue, Token, Type}; 5 | 6 | use quote::{format_ident, quote, ToTokens, TokenStreamExt}; 7 | 8 | use crate::parse_string_meta; 9 | use crate::struct_gen::struct_buildable_value_gen::gen_impl_buildable_value; 10 | 11 | mod named_fields; 12 | mod struct_buildable_value_gen; 13 | mod unnamed_fields; 14 | 15 | /// Generator for all the builder-related tokens for a `struct`. It will generate a new struct that 16 | /// implements `BuildableValue`, and will implement some traits for the various types in the game. 17 | #[derive(Debug)] 18 | pub struct StructGenerator { 19 | /// The `Ident` of the original struct. 20 | ident: Ident, 21 | /// The `Ident` of the newly created struct. 22 | builder_ident: Ident, 23 | /// The list of fields in the original struct. 24 | fields: Vec, 25 | /// The span of this structure. 26 | span: Span, 27 | /// Whether the fields of this struct are named. 28 | named_fields: bool, 29 | /// The metadata associated with the struct. 30 | metadata: StructMetadata, 31 | } 32 | 33 | /// The metadata of the struct, it's taken from the attributes of the `struct`. 34 | #[derive(Debug)] 35 | pub struct StructMetadata { 36 | /// The prompt to use for this struct's main menu. 37 | prompt: Option, 38 | /// Different name to use in the tree structure. 39 | rename: Option, 40 | } 41 | 42 | /// The information about a field of a struct. 43 | #[derive(Debug)] 44 | pub struct StructField { 45 | /// The `Ident` of the field. It's `None` if the struct has unnamed fields (`struct Foo(i64)`). 46 | pub ident: Option, 47 | /// The type of the field. 48 | pub ty: Type, 49 | /// The whole field that generated this instance. 50 | pub field: Field, 51 | /// The metadata associated with the field. 52 | pub metadata: FieldMetadata, 53 | } 54 | 55 | /// The metadata of the field, it's taken from the attributes of the `Field`. 56 | #[derive(Debug)] 57 | pub struct FieldMetadata { 58 | /// The default value for this field. 59 | pub default: Option, 60 | /// The prompt to use for this field. 61 | pub prompt: Option, 62 | /// Different name to use in the tree structure. 63 | pub rename: Option, 64 | /// Whether this field is hidden. 65 | pub hidden: bool, 66 | } 67 | 68 | /// Generator for the list of field definition of a struct. It will generate either: 69 | /// - `{ name: Type, name: Type, ... }` 70 | /// - `(Type, Type, ...)` 71 | /// 72 | /// Where `Type` is the type of the generator that is able to build the corresponding field of the 73 | /// original struct. For builtin types it's `ibuilder::builders::XXXBuilder`, for the custom types 74 | /// it's `Box`. 75 | struct FieldDefList<'s> { 76 | /// A reference to the list of fields of the struct. 77 | fields: &'s [StructField], 78 | /// Whether the struct has named fields or not. 79 | named: bool, 80 | } 81 | 82 | /// Generator for the list of field creation of a struct. It will generate either: 83 | /// - `{ name: Type::new(...), ... }` 84 | /// - `(Type::new(...), ...)` 85 | /// 86 | /// Where the `Type::new` will be chosen according to the type of the field in the original struct 87 | /// and eventually will forward the `FieldMetadata`. 88 | struct FieldNewList<'s> { 89 | /// A reference to the original generator for the struct. 90 | gen: &'s StructGenerator, 91 | } 92 | 93 | /// Generator for the `impl std::fmt::Debug for ...` implementation block. This will generate a 94 | /// Debug implementation for all the fields, but the hidden ones. 95 | struct ImplDebug<'s> { 96 | /// A reference to the original generator for the struct. 97 | gen: &'s StructGenerator, 98 | } 99 | 100 | impl StructGenerator { 101 | /// Generate the `Ident` to use as the implementation of `BuildableValue` for a struct. 102 | pub fn gen_builder_ident(ident: &Ident) -> Ident { 103 | format_ident!("__{}_BuildableValueImpl", ident) 104 | } 105 | 106 | /// Construct a new `StructGenerator` from the AST of a struct. Will fail if the AST is not 107 | /// relative to a struct. 108 | pub fn from_struct(ast: &syn::DeriveInput) -> StructGenerator { 109 | match &ast.data { 110 | syn::Data::Struct(data) => { 111 | let named_fields = matches!(data.fields, Fields::Named(_)); 112 | let metadata = StructMetadata::from(ast); 113 | StructGenerator { 114 | ident: ast.ident.clone(), 115 | builder_ident: StructGenerator::gen_builder_ident(&ast.ident), 116 | fields: match &data.fields { 117 | syn::Fields::Named(fields) => { 118 | fields.named.iter().map(StructField::from).collect() 119 | } 120 | syn::Fields::Unnamed(fields) => { 121 | let mut fields: Vec<_> = 122 | fields.unnamed.iter().map(StructField::from).collect(); 123 | // forward the prompt to the unnamed fields to avoid having to add the 124 | // attribute for the field (i.e. inside the parenthesis). 125 | if let Some(prompt) = &metadata.prompt { 126 | for field in fields.iter_mut() { 127 | if field.metadata.prompt.is_none() { 128 | field.metadata.prompt = Some(prompt.clone()); 129 | } 130 | } 131 | } 132 | fields 133 | } 134 | syn::Fields::Unit => vec![], 135 | }, 136 | span: ast.ident.span(), 137 | named_fields, 138 | metadata, 139 | } 140 | } 141 | _ => panic!("expecting a struct"), 142 | } 143 | } 144 | 145 | /// Return the actual name of the struct, which is the defined name or the renamed one. The 146 | /// string literal of the name is returned. 147 | fn actual_name(&self) -> TokenStream { 148 | if let Some(renamed) = &self.metadata.rename { 149 | quote! { #renamed } 150 | } else { 151 | let ident = self.ident.to_string(); 152 | quote! { #ident } 153 | } 154 | } 155 | 156 | /// Whether the fields of the original struct are named or unnamed (`struct Foo(i64)`). 157 | fn is_named(&self) -> bool { 158 | self.named_fields 159 | } 160 | 161 | /// Make a new `FieldDefList` relative to this struct. 162 | fn fields_def_list(&self) -> FieldDefList { 163 | FieldDefList { 164 | fields: &self.fields, 165 | named: self.is_named(), 166 | } 167 | } 168 | 169 | /// Make a new `FieldNewList` relative to this struct. 170 | fn fields_new_list(&self) -> FieldNewList { 171 | FieldNewList { gen: self } 172 | } 173 | 174 | /// Make a new `ImplDebug` for to this struct. 175 | /// 176 | /// This implements the `Debug` trait without requiring any field to be `Debug`. The basic field 177 | /// must be `Debug`, but the hidden ones don't have to. 178 | fn impl_debug(&self) -> ImplDebug { 179 | ImplDebug { gen: self } 180 | } 181 | } 182 | 183 | impl From<&syn::DeriveInput> for StructMetadata { 184 | fn from(data: &syn::DeriveInput) -> StructMetadata { 185 | let mut metadata = StructMetadata { 186 | prompt: None, 187 | rename: None, 188 | }; 189 | for attr in &data.attrs { 190 | if attr.path.is_ident("ibuilder") { 191 | let meta = attr 192 | .parse_args_with(Punctuated::::parse_terminated) 193 | .unwrap_or_abort(); 194 | for meta in meta { 195 | parse_struct_meta(meta, &mut metadata); 196 | } 197 | } 198 | } 199 | metadata 200 | } 201 | } 202 | 203 | /// Extract the `StructMetadata` from a `Meta` entry in an attribute. `meta` comes from 204 | /// `#[ibuilder(HERE)]`. 205 | fn parse_struct_meta(meta: Meta, metadata: &mut StructMetadata) { 206 | match meta { 207 | Meta::NameValue(MetaNameValue { path, lit, .. }) => { 208 | if path.is_ident("prompt") { 209 | parse_string_meta(&mut metadata.prompt, lit); 210 | } else if path.is_ident("rename") { 211 | parse_string_meta(&mut metadata.rename, lit); 212 | } else { 213 | abort!(path, "unknown attribute"); 214 | } 215 | } 216 | _ => abort!(meta, "unknown attribute"), 217 | } 218 | } 219 | 220 | impl StructField { 221 | /// The type of the builder for the type of this field. It's either one of the builtin types, a 222 | /// generic boxed one, or the actual type if the field is hidden. 223 | fn builder_type(&self) -> TokenStream { 224 | if self.metadata.hidden { 225 | let ty = &self.ty; 226 | quote! { #ty } 227 | } else if let Some(builtin) = self.builtin_type() { 228 | quote! { #builtin } 229 | } else { 230 | quote! { Box } 231 | } 232 | } 233 | 234 | /// The initializer of the builder for the current field. It will forward the `FieldMetadata` 235 | /// to the builder. 236 | fn builder_new(&self) -> TokenStream { 237 | let prompt = match &self.metadata.prompt { 238 | Some(prompt) => quote!(Some(#prompt.to_string())), 239 | None => quote! {None}, 240 | }; 241 | if self.metadata.hidden { 242 | return if let Some(default) = &self.metadata.default { 243 | quote! { #default } 244 | } else { 245 | quote! { ::std::default::Default::default() } 246 | }; 247 | } 248 | if let Some(builtin) = self.builtin_type() { 249 | let default = if let Some(default) = self.metadata.default.clone() { 250 | quote! { Some(#default) } 251 | } else { 252 | quote! { None } 253 | }; 254 | quote! { 255 | <#builtin>::new(ibuilder::BuildableValueConfig { 256 | default: #default, 257 | prompt: #prompt, 258 | }) 259 | } 260 | } else { 261 | let ty = &self.ty; 262 | quote! { 263 | <#ty as ibuilder::NewBuildableValue>::new_buildable_value(ibuilder::BuildableValueConfig { 264 | default: None, 265 | prompt: #prompt, 266 | }) 267 | } 268 | } 269 | } 270 | 271 | /// Check if the type of the field is a builtin type, and in this case it will return the 272 | /// corresponding builder. It returns `None` if it's not a builtin type. 273 | fn builtin_type(&self) -> Option { 274 | match &self.ty { 275 | Type::Path(path) => { 276 | let segments = &path.path.segments; 277 | if segments.len() != 1 { 278 | return None; 279 | } 280 | let ty = segments[0].ident.to_string(); 281 | let ty = ty.as_str(); 282 | match ty { 283 | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" 284 | | "usize" | "f32" | "f64" | "String" | "char" | "bool" => { 285 | let builder = 286 | format_ident!("{}", ty[0..1].to_uppercase() + &ty[1..] + "Builder"); 287 | Some(quote! { ibuilder::builders::#builder }) 288 | } 289 | _ => None, 290 | } 291 | } 292 | _ => None, 293 | } 294 | } 295 | 296 | /// Return the actual name of the field, which is the defined name or the renamed one. The 297 | /// string literal of the name is returned. 298 | fn actual_name(&self) -> TokenStream { 299 | if let Some(renamed) = &self.metadata.rename { 300 | quote! { #renamed } 301 | } else { 302 | let ident = self.ident.as_ref().unwrap().to_string(); 303 | quote! { #ident } 304 | } 305 | } 306 | } 307 | 308 | impl ToTokens for StructGenerator { 309 | fn to_tokens(&self, tokens: &mut TokenStream) { 310 | tokens.append_all(gen_struct_builder(self)); 311 | tokens.append_all(gen_impl_new_buildable_value(self)); 312 | tokens.append_all(gen_impl_buildable_value(self)); 313 | } 314 | } 315 | 316 | impl<'s> ToTokens for FieldDefList<'s> { 317 | fn to_tokens(&self, tokens: &mut TokenStream) { 318 | // an empty struct is declared like `struct Foo;` 319 | if self.fields.is_empty() { 320 | tokens.append_all(quote! { ; }); 321 | return; 322 | } 323 | let mut inner = TokenStream::new(); 324 | for field in self.fields { 325 | // named field: prepend the field name 326 | if let Some(ident) = &field.ident { 327 | inner.append_all(quote! {#ident: }); 328 | } 329 | let ty = field.builder_type(); 330 | inner.append_all(quote! {#ty,}) 331 | } 332 | if self.named { 333 | inner.append_all(quote! { __prompt: String, }); 334 | tokens.append_all(quote! { { #inner } }); 335 | } else { 336 | // unnamed struct has the prompt directly forwarded to the inner type 337 | tokens.append_all(quote! { ( #inner ); }); 338 | } 339 | } 340 | } 341 | 342 | impl<'s> ToTokens for FieldNewList<'s> { 343 | fn to_tokens(&self, tokens: &mut TokenStream) { 344 | if self.gen.fields.is_empty() { 345 | tokens.append_all(quote! {}); 346 | return; 347 | } 348 | let prompt = &self.gen.metadata.prompt.as_deref(); 349 | let prompt = prompt.unwrap_or("Select the field to edit"); 350 | let prompt = quote! { config.prompt.unwrap_or_else(|| #prompt.to_string()) }; 351 | let mut inner = TokenStream::new(); 352 | for field in &self.gen.fields { 353 | // named field: prepend the field name 354 | if let Some(ident) = &field.ident { 355 | inner.append_all(quote! {#ident: }); 356 | } 357 | let init = field.builder_new(); 358 | inner.append_all(quote! {#init,}) 359 | } 360 | if self.gen.is_named() { 361 | inner.append_all(quote! { __prompt: #prompt, }); 362 | tokens.append_all(quote! { { #inner } }); 363 | } else { 364 | tokens.append_all(quote! { ( #inner ) }); 365 | } 366 | } 367 | } 368 | 369 | impl<'s> ToTokens for ImplDebug<'s> { 370 | #[allow(clippy::collapsible_else_if)] 371 | fn to_tokens(&self, tokens: &mut TokenStream) { 372 | let builder_ident = &self.gen.builder_ident; 373 | let mut fields = TokenStream::new(); 374 | for (i, field) in self.gen.fields.iter().enumerate() { 375 | if let Some(ident) = &field.ident { 376 | if field.metadata.hidden { 377 | fields.append_all(quote! { .field(stringify!(#ident), &"[hidden]") }); 378 | } else { 379 | fields.append_all(quote! { .field(stringify!(#ident), &self.#ident) }); 380 | } 381 | } else { 382 | if field.metadata.hidden { 383 | fields.append_all(quote! { .field(stringify!(#i), &"[hidden]") }); 384 | } else { 385 | let index = syn::Index::from(i); 386 | fields.append_all(quote! { .field(stringify!(#i), &self.#index) }); 387 | } 388 | } 389 | } 390 | tokens.append_all(quote! { 391 | #[automatically_derived] 392 | impl std::fmt::Debug for #builder_ident { 393 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 394 | f.debug_struct(stringify!(#builder_ident)) 395 | #fields 396 | .finish() 397 | } 398 | } 399 | }) 400 | } 401 | } 402 | 403 | impl From<&Field> for StructField { 404 | fn from(field: &Field) -> StructField { 405 | let res = StructField { 406 | ident: field.ident.clone(), 407 | ty: field.ty.clone(), 408 | field: field.clone(), 409 | metadata: get_field_metadata(field), 410 | }; 411 | if res.metadata.default.is_some() && res.builtin_type().is_none() { 412 | abort!(field, "default value is supported only on plain types"); 413 | } 414 | res 415 | } 416 | } 417 | 418 | /// Extract the `FieldMetadata` from the attribute list of a field. 419 | fn get_field_metadata(field: &Field) -> FieldMetadata { 420 | let mut metadata = FieldMetadata { 421 | default: None, 422 | prompt: None, 423 | rename: None, 424 | hidden: false, 425 | }; 426 | for attr in &field.attrs { 427 | if attr.path.is_ident("ibuilder") { 428 | let meta = attr 429 | .parse_args_with(Punctuated::::parse_terminated) 430 | .unwrap_or_abort(); 431 | for meta in meta { 432 | parse_field_meta(meta, &mut metadata, &field.ty); 433 | } 434 | } 435 | } 436 | if metadata.hidden && field.ident.is_none() { 437 | abort!(field, "unnamed fields cannot be hidden"); 438 | } 439 | metadata 440 | } 441 | 442 | /// Extract the `FieldMetadata` from a `Meta` entry in a field attribute. `meta` comes from 443 | /// `#[ibuilder(HERE)]`. 444 | fn parse_field_meta(meta: Meta, metadata: &mut FieldMetadata, ty: &Type) { 445 | match meta { 446 | Meta::NameValue(MetaNameValue { path, lit, .. }) => { 447 | if path.is_ident("default") { 448 | if metadata.default.is_none() { 449 | match lit { 450 | syn::Lit::Str(_) => { 451 | metadata.default = 452 | Some(quote! { <#ty as std::str::FromStr>::from_str(#lit).unwrap() }) 453 | } 454 | _ => metadata.default = Some(quote! { #lit }), 455 | } 456 | } else { 457 | abort!(path, "duplicated default"); 458 | } 459 | } else if path.is_ident("prompt") { 460 | parse_string_meta(&mut metadata.prompt, lit); 461 | } else if path.is_ident("rename") { 462 | parse_string_meta(&mut metadata.rename, lit); 463 | } else { 464 | abort!(path, "unknown attribute"); 465 | } 466 | } 467 | Meta::Path(path) => { 468 | if path.is_ident("hidden") { 469 | if metadata.hidden { 470 | emit_warning!(path, "duplicated attribute"); 471 | } 472 | metadata.hidden = true; 473 | } else { 474 | abort!(path, "unknown attribute"); 475 | } 476 | } 477 | _ => abort!(meta, "unknown attribute"), 478 | } 479 | } 480 | 481 | /// Generate the struct that implements `BuildableValue` for the struct, and implement the `new()` 482 | /// function for it. 483 | fn gen_struct_builder(gen: &StructGenerator) -> TokenStream { 484 | let builder_ident = &gen.builder_ident; 485 | let fields_gen = gen.fields_def_list(); 486 | let fields_new = gen.fields_new_list(); 487 | let impl_debug = gen.impl_debug(); 488 | quote! { 489 | #[automatically_derived] 490 | #[allow(non_camel_case_types)] 491 | #[doc(hidden)] 492 | struct #builder_ident #fields_gen 493 | 494 | #impl_debug 495 | 496 | #[automatically_derived] 497 | #[allow(clippy::unnecessary_cast)] 498 | impl #builder_ident { 499 | fn new(config: ibuilder::BuildableValueConfig<()>) -> #builder_ident { 500 | #builder_ident #fields_new 501 | } 502 | } 503 | } 504 | } 505 | 506 | /// Generate the implementation of `NewBuildableValue` for the struct. 507 | fn gen_impl_new_buildable_value(gen: &StructGenerator) -> TokenStream { 508 | let ident = &gen.ident; 509 | let builder_ident = &gen.builder_ident; 510 | quote! { 511 | #[automatically_derived] 512 | impl ibuilder::NewBuildableValue for #ident { 513 | fn new_buildable_value(config: ibuilder::BuildableValueConfig<()>) -> Box { 514 | Box::new(#builder_ident::new(config)) 515 | } 516 | } 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /ibuilder/src/builders.rs: -------------------------------------------------------------------------------- 1 | //! Module with the implementors of `BuildableValue` for the various standard types. 2 | 3 | use std::any::Any; 4 | use std::marker::PhantomData; 5 | use std::path::PathBuf; 6 | use std::str::FromStr; 7 | 8 | use crate::nodes::{Field, FieldKind, Node}; 9 | use crate::{ 10 | BuildableValue, BuildableValueConfig, Choice, ChooseError, Input, NewBuildableValue, Options, 11 | }; 12 | 13 | macro_rules! type_builder_boilerplate { 14 | (normal) => { 15 | fn get_subfields(&self, _: &[String]) -> Vec { 16 | vec![] 17 | } 18 | 19 | fn to_node(&self) -> Node { 20 | if let Some(value) = &self.value { 21 | Node::Leaf(Field::String(value.to_string())) 22 | } else { 23 | Node::Leaf(Field::Missing) 24 | } 25 | } 26 | }; 27 | (path) => { 28 | fn get_subfields(&self, _: &[String]) -> Vec { 29 | vec![] 30 | } 31 | 32 | fn to_node(&self) -> Node { 33 | if let Some(value) = &self.value { 34 | Node::Leaf(Field::String( 35 | value.as_os_str().to_string_lossy().to_string(), 36 | )) 37 | } else { 38 | Node::Leaf(Field::Missing) 39 | } 40 | } 41 | }; 42 | } 43 | 44 | macro_rules! type_builder_struct { 45 | ($base:ty, $name:ident, $query:expr) => { 46 | type_builder_struct!( 47 | $base, 48 | $name, 49 | $query, 50 | concat!("Builder for the type `", stringify!($base), "`") 51 | ); 52 | }; 53 | ($base:ty, $name:ident, $query:expr, $docstring:expr) => { 54 | #[doc = $docstring] 55 | #[derive(Debug)] 56 | pub struct $name { 57 | /// The current value. 58 | pub value: Option<$base>, 59 | /// The message to show to the user. 60 | pub prompt: String, 61 | } 62 | 63 | impl $name { 64 | /// Make a new instance of the builder. 65 | pub fn new(config: BuildableValueConfig<$base>) -> Self { 66 | Self { 67 | value: config.default, 68 | prompt: config.prompt.unwrap_or_else(|| $query.to_string()), 69 | } 70 | } 71 | } 72 | }; 73 | } 74 | 75 | macro_rules! type_builder { 76 | ($base:ty, $name:ident, $query:expr) => { 77 | type_builder!( 78 | $base, 79 | $name, 80 | $query, 81 | normal 82 | ); 83 | }; 84 | ($base:ty, $name:ident, $query:expr, $variant:tt) => { 85 | type_builder!( 86 | @, 87 | $base, 88 | $name, 89 | $query, 90 | concat!("Builder for the type `", stringify!($base), "`"), 91 | $variant 92 | ); 93 | }; 94 | (@, $base:ty, $name:ident, $query:expr, $docstring:expr, $variant:tt) => { 95 | type_builder_struct!($base, $name, $query, $docstring); 96 | 97 | impl BuildableValue for $name { 98 | type_builder_boilerplate!($variant); 99 | 100 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError> { 101 | if !current_fields.is_empty() { 102 | panic!( 103 | "{}.apply() called with non empty fields: {:?}", 104 | stringify!($name), 105 | current_fields 106 | ); 107 | } 108 | match data { 109 | Input::Text(data) => { 110 | self.value = Some(<$base>::from_str(&data).map_err(|e| { 111 | ChooseError::InvalidText { 112 | error: e.to_string(), 113 | } 114 | })?); 115 | } 116 | _ => return Err(ChooseError::UnexpectedChoice), 117 | } 118 | Ok(()) 119 | } 120 | 121 | fn get_options(&self, current_fields: &[String]) -> Options { 122 | if !current_fields.is_empty() { 123 | panic!( 124 | "{}.get_options() called with non empty fields: {:?}", 125 | stringify!($name), 126 | current_fields 127 | ); 128 | } 129 | Options { 130 | query: self.prompt.clone(), 131 | text_input: true, 132 | choices: vec![], 133 | } 134 | } 135 | 136 | fn get_value_any(&self) -> Option> { 137 | self.value.clone().map(|x| Box::new(x) as Box) 138 | } 139 | } 140 | 141 | impl NewBuildableValue for $base { 142 | fn new_buildable_value(config: BuildableValueConfig<()>) -> Box { 143 | Box::new($name::new(BuildableValueConfig { 144 | default: None, 145 | prompt: config.prompt, 146 | })) 147 | } 148 | } 149 | }; 150 | } 151 | 152 | type_builder!(i8, I8Builder, "Type an integer"); 153 | type_builder!(i16, I16Builder, "Type an integer"); 154 | type_builder!(i32, I32Builder, "Type an integer"); 155 | type_builder!(i64, I64Builder, "Type an integer"); 156 | type_builder!(u8, U8Builder, "Type an integer"); 157 | type_builder!(u16, U16Builder, "Type an integer"); 158 | type_builder!(u32, U32Builder, "Type an integer"); 159 | type_builder!(u64, U64Builder, "Type an integer"); 160 | type_builder!(isize, IsizeBuilder, "Type an integer"); 161 | type_builder!(usize, UsizeBuilder, "Type an integer"); 162 | type_builder!(f32, F32Builder, "Type an integer"); 163 | type_builder!(f64, F64Builder, "Type an integer"); 164 | type_builder!(String, StringBuilder, "Type a string"); 165 | type_builder!(char, CharBuilder, "Type a char"); 166 | type_builder!(PathBuf, PathBufBuilder, "Type a path", path); 167 | 168 | type_builder_struct!(bool, BoolBuilder, "True or false?"); 169 | 170 | impl BuildableValue for BoolBuilder { 171 | type_builder_boilerplate!(normal); 172 | 173 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError> { 174 | if !current_fields.is_empty() { 175 | panic!( 176 | "BoolBuilder.apply() called with non empty fields: {:?}", 177 | current_fields 178 | ); 179 | } 180 | match data { 181 | Input::Choice(data) => match data.as_str() { 182 | "true" => self.value = Some(true), 183 | "false" => self.value = Some(false), 184 | _ => return Err(ChooseError::UnexpectedChoice), 185 | }, 186 | Input::Text(_) => return Err(ChooseError::UnexpectedText), 187 | } 188 | Ok(()) 189 | } 190 | 191 | fn get_options(&self, current_fields: &[String]) -> Options { 192 | if !current_fields.is_empty() { 193 | panic!( 194 | "BoolBuilder.get_options() called with non empty fields: {:?}", 195 | current_fields 196 | ); 197 | } 198 | Options { 199 | query: self.prompt.clone(), 200 | text_input: false, 201 | choices: vec![ 202 | Choice { 203 | choice_id: "true".to_string(), 204 | text: "true".to_string(), 205 | needs_action: false, 206 | }, 207 | Choice { 208 | choice_id: "false".to_string(), 209 | text: "false".to_string(), 210 | needs_action: false, 211 | }, 212 | ], 213 | } 214 | } 215 | 216 | fn get_value_any(&self) -> Option> { 217 | self.value.map(|x| Box::new(x) as Box) 218 | } 219 | } 220 | 221 | /// Builder for the type `Vec`. 222 | /// 223 | /// The type parameters are: 224 | /// - `B`: the type of the builder that produces `T` 225 | /// - `T`: the type of the items of the final `Vec` 226 | /// 227 | /// The state machine that this builder implements is a bit complex since it has to handle 228 | /// insertions, deletions and updates and it looks like this: 229 | /// 230 | /// ```text 231 | /// | 232 | /// v 233 | /// +-------------+ __new +-------------+ specific 234 | /// +-------> | empty | ----------> | not empty | ------>>> 235 | /// | | main | | edit | 236 | /// | +-------------+ +-------------+ 237 | /// | ^ | 238 | /// | | | 239 | /// | +-------------+ index / __new | | __back 240 | /// +-------> | not empty | ----------------+ | 241 | /// | | main | <--------------------+ 242 | /// | index +-------------+ 243 | /// | ^ | 244 | /// | __back | | __remove 245 | /// | | v 246 | /// | +-------------+ 247 | /// +-------- | not empty | 248 | /// | remove | 249 | /// +-------------+ 250 | /// ``` 251 | /// 252 | /// When `__new` is applied a new item is pushed at the back of the `Vec` and when `__new` is to 253 | /// be considered as an index it refers to the last element of the `Vec`. 254 | pub struct VecBuilder 255 | where 256 | T: NewBuildableValue + 'static, 257 | { 258 | items: Vec>, 259 | inner_type: PhantomData, 260 | prompt: String, 261 | } 262 | 263 | impl std::fmt::Debug for VecBuilder 264 | where 265 | T: NewBuildableValue + 'static, 266 | { 267 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 268 | f.debug_struct("VecBuilder") 269 | .field("items", &self.items) 270 | .finish() 271 | } 272 | } 273 | 274 | impl NewBuildableValue for Vec 275 | where 276 | T: NewBuildableValue + 'static, 277 | { 278 | fn new_buildable_value(config: BuildableValueConfig<()>) -> Box { 279 | Box::new(VecBuilder:: { 280 | items: Vec::new(), 281 | inner_type: Default::default(), 282 | prompt: config 283 | .prompt 284 | .unwrap_or_else(|| "Select an action".to_string()), 285 | }) 286 | } 287 | } 288 | 289 | impl BuildableValue for VecBuilder 290 | where 291 | T: NewBuildableValue + 'static, 292 | { 293 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError> { 294 | // vec main menu 295 | if current_fields.is_empty() { 296 | match data { 297 | Input::Choice(data) if data == "__new" => { 298 | self.items.push(T::new_buildable_value(Default::default())); 299 | } 300 | Input::Choice(data) => { 301 | if data != "__remove" { 302 | // check that the inserted index is valid 303 | let index = 304 | usize::from_str(&data).map_err(|_| ChooseError::UnexpectedChoice)?; 305 | if index >= self.items.len() { 306 | return Err(ChooseError::UnexpectedChoice); 307 | } 308 | } 309 | } 310 | _ => return Err(ChooseError::UnexpectedText), 311 | } 312 | // remove item or apply to element 313 | } else { 314 | let field = ¤t_fields[0]; 315 | let rest = ¤t_fields[1..]; 316 | match field.as_str() { 317 | "__remove" => match data { 318 | Input::Choice(choice) => { 319 | let index = 320 | usize::from_str(&choice).map_err(|_| ChooseError::UnexpectedChoice)?; 321 | if index >= self.items.len() { 322 | return Err(ChooseError::UnexpectedChoice); 323 | } 324 | self.items.remove(index); 325 | } 326 | Input::Text(_) => return Err(ChooseError::UnexpectedText), 327 | }, 328 | "__new" => { 329 | self.items 330 | .last_mut() 331 | .expect("Vec __new didn't push") 332 | .apply(data, rest)?; 333 | } 334 | index => { 335 | let index = usize::from_str(index) 336 | .unwrap_or_else(|_| panic!("Invalid index for vec: {}", index)); 337 | self.items[index].apply(data, rest)?; 338 | } 339 | } 340 | } 341 | Ok(()) 342 | } 343 | 344 | fn get_options(&self, current_fields: &[String]) -> Options { 345 | // vec main manu 346 | if current_fields.is_empty() { 347 | let mut choices = vec![Choice { 348 | choice_id: "__new".to_string(), 349 | text: "New element".to_string(), 350 | needs_action: false, 351 | }]; 352 | if !self.items.is_empty() { 353 | choices.push(Choice { 354 | choice_id: "__remove".to_string(), 355 | text: "Remove element".to_string(), 356 | needs_action: false, 357 | }); 358 | for i in 0..self.items.len() { 359 | choices.push(Choice { 360 | choice_id: i.to_string(), 361 | text: format!("Edit item {}", i), 362 | needs_action: self.items[i].get_value_any().is_none(), 363 | }); 364 | } 365 | } 366 | Options { 367 | query: self.prompt.clone(), 368 | text_input: false, 369 | choices, 370 | } 371 | // item menu 372 | } else { 373 | let field = ¤t_fields[0]; 374 | let rest = ¤t_fields[1..]; 375 | match field.as_str() { 376 | // select the item to remove 377 | "__remove" => { 378 | let mut choices = Vec::new(); 379 | for i in 0..self.items.len() { 380 | choices.push(Choice { 381 | choice_id: i.to_string(), 382 | text: format!("Remove item {}", i), 383 | needs_action: false, 384 | }); 385 | } 386 | Options { 387 | query: "Select the item to remove".to_string(), 388 | text_input: false, 389 | choices, 390 | } 391 | } 392 | // last action was __new, now inside the last item menu 393 | "__new" => self 394 | .items 395 | .last() 396 | .expect("Vec __new didn't push") 397 | .get_options(rest), 398 | // edit one of the items 399 | index => { 400 | let index = usize::from_str(index) 401 | .unwrap_or_else(|_| panic!("Invalid index for vec: {}", index)); 402 | self.items[index].get_options(rest) 403 | } 404 | } 405 | } 406 | } 407 | 408 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 409 | // main manu 410 | if current_fields.is_empty() { 411 | if self.items.is_empty() { 412 | vec!["__new".into()] 413 | } else { 414 | let mut res = vec!["__new".into(), "__remove".into()]; 415 | for i in 0..self.items.len() { 416 | res.push(i.to_string()); 417 | } 418 | res 419 | } 420 | } else { 421 | let field = ¤t_fields[0]; 422 | let rest = ¤t_fields[1..]; 423 | match field.as_str() { 424 | // just select the item to remove 425 | "__remove" => vec![], 426 | "__new" => self 427 | .items 428 | .last() 429 | .expect("Vec __new didn't push") 430 | .get_subfields(rest), 431 | index => { 432 | let index = usize::from_str(index) 433 | .unwrap_or_else(|_| panic!("Invalid index for vec: {}", index)); 434 | self.items[index].get_subfields(rest) 435 | } 436 | } 437 | } 438 | } 439 | 440 | fn to_node(&self) -> Node { 441 | let items = self 442 | .items 443 | .iter() 444 | .map(|i| FieldKind::Unnamed(i.to_node())) 445 | .collect(); 446 | // Vec has no name 447 | Node::Composite("".into(), items) 448 | } 449 | 450 | fn get_value_any(&self) -> Option> { 451 | let mut results: Vec = Vec::with_capacity(self.items.len()); 452 | for item in &self.items { 453 | results.push(*item.get_value_any()?.downcast::().unwrap()); 454 | } 455 | Some(Box::new(results)) 456 | } 457 | } 458 | 459 | /// Builder for the type `Box`. 460 | pub struct BoxBuilder 461 | where 462 | T: NewBuildableValue + 'static, 463 | { 464 | value: Box, 465 | inner_type: PhantomData, 466 | } 467 | 468 | impl std::fmt::Debug for BoxBuilder 469 | where 470 | T: NewBuildableValue + 'static, 471 | { 472 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 473 | f.debug_struct("BoxBuilder") 474 | .field("value", &self.value) 475 | .finish() 476 | } 477 | } 478 | 479 | impl NewBuildableValue for Box 480 | where 481 | T: NewBuildableValue + 'static, 482 | { 483 | fn new_buildable_value(config: BuildableValueConfig<()>) -> Box { 484 | Box::new(BoxBuilder:: { 485 | value: T::new_buildable_value(config), 486 | inner_type: Default::default(), 487 | }) 488 | } 489 | } 490 | 491 | impl BuildableValue for BoxBuilder 492 | where 493 | T: NewBuildableValue + 'static, 494 | { 495 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError> { 496 | self.value.apply(data, current_fields) 497 | } 498 | 499 | fn get_options(&self, current_fields: &[String]) -> Options { 500 | self.value.get_options(current_fields) 501 | } 502 | 503 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 504 | self.value.get_subfields(current_fields) 505 | } 506 | 507 | fn to_node(&self) -> Node { 508 | self.value.to_node() 509 | } 510 | 511 | fn get_value_any(&self) -> Option> { 512 | Some(Box::new(Box::new( 513 | *self.value.get_value_any()?.downcast::().unwrap(), 514 | ))) 515 | } 516 | } 517 | 518 | /// Builder for the type `Option`. 519 | pub struct OptionBuilder 520 | where 521 | T: NewBuildableValue + 'static, 522 | { 523 | value: Option>, 524 | inner_type: PhantomData, 525 | prompt: String, 526 | } 527 | 528 | impl std::fmt::Debug for OptionBuilder 529 | where 530 | T: NewBuildableValue + 'static, 531 | { 532 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 533 | f.debug_struct("OptionBuilder") 534 | .field("value", &self.value) 535 | .finish() 536 | } 537 | } 538 | 539 | impl NewBuildableValue for Option 540 | where 541 | T: NewBuildableValue + 'static, 542 | { 543 | fn new_buildable_value(config: BuildableValueConfig<()>) -> Box { 544 | Box::new(OptionBuilder:: { 545 | value: None, 546 | inner_type: Default::default(), 547 | prompt: config 548 | .prompt 549 | .unwrap_or_else(|| "Choose an option".to_string()), 550 | }) 551 | } 552 | } 553 | 554 | impl BuildableValue for OptionBuilder 555 | where 556 | T: NewBuildableValue + 'static, 557 | { 558 | fn apply(&mut self, data: Input, current_fields: &[String]) -> Result<(), ChooseError> { 559 | if current_fields.is_empty() { 560 | match data { 561 | Input::Choice(data) => match data.as_str() { 562 | "__remove" => self.value = None, 563 | "__edit" => {} 564 | "__set" => self.value = Some(T::new_buildable_value(Default::default())), 565 | _ => return Err(ChooseError::UnexpectedChoice), 566 | }, 567 | Input::Text(_) => return Err(ChooseError::UnexpectedText), 568 | } 569 | Ok(()) 570 | } else { 571 | let field = ¤t_fields[0]; 572 | let rest = ¤t_fields[1..]; 573 | if field == "__edit" || field == "__set" { 574 | self.value.as_mut().unwrap().apply(data, rest) 575 | } else { 576 | unreachable!("Unexpected field: {}", field); 577 | } 578 | } 579 | } 580 | 581 | fn get_options(&self, current_fields: &[String]) -> Options { 582 | if current_fields.is_empty() { 583 | let choices = match self.value { 584 | Some(_) => vec![ 585 | Choice { 586 | choice_id: "__remove".to_string(), 587 | text: "Remove value".to_string(), 588 | needs_action: false, 589 | }, 590 | Choice { 591 | choice_id: "__edit".to_string(), 592 | text: "Edit value".to_string(), 593 | needs_action: false, 594 | }, 595 | ], 596 | None => vec![Choice { 597 | choice_id: "__set".to_string(), 598 | text: "Set value".to_string(), 599 | needs_action: false, 600 | }], 601 | }; 602 | Options { 603 | query: self.prompt.clone(), 604 | text_input: false, 605 | choices, 606 | } 607 | } else { 608 | let field = ¤t_fields[0]; 609 | let rest = ¤t_fields[1..]; 610 | if field == "__edit" || field == "__set" { 611 | self.value.as_ref().unwrap().get_options(rest) 612 | } else { 613 | unreachable!("Unexpected field: {}", field); 614 | } 615 | } 616 | } 617 | 618 | fn get_subfields(&self, current_fields: &[String]) -> Vec { 619 | if current_fields.is_empty() { 620 | match self.value { 621 | Some(_) => vec!["__edit".to_string()], 622 | None => vec!["__set".to_string()], 623 | } 624 | } else { 625 | let field = ¤t_fields[0]; 626 | let rest = ¤t_fields[1..]; 627 | if field == "__edit" || field == "__set" { 628 | self.value.as_ref().unwrap().get_subfields(rest) 629 | } else { 630 | unreachable!("Unexpected field: {}", field); 631 | } 632 | } 633 | } 634 | 635 | fn to_node(&self) -> Node { 636 | match &self.value { 637 | Some(inner) => inner.to_node(), 638 | None => Node::Leaf(Field::String("None".into())), 639 | } 640 | } 641 | 642 | fn get_value_any(&self) -> Option> { 643 | match &self.value { 644 | Some(inner) => Some(Box::new(Some( 645 | *inner.get_value_any()?.downcast::().unwrap(), 646 | ))), 647 | None => Some(Box::new(None::)), 648 | } 649 | } 650 | } 651 | --------------------------------------------------------------------------------