Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move from pass-by-value builder style API to pass-by-reference #220

Open
dralley opened this issue Mar 10, 2024 · 4 comments
Open

Move from pass-by-value builder style API to pass-by-reference #220

dralley opened this issue Mar 10, 2024 · 4 comments

Comments

@dralley
Copy link
Collaborator

dralley commented Mar 10, 2024

The current style isn't very readable for RPMs of even average complexity, e.g.

 let built_rpm = PackageBuilder::new(
        "rpm-basic",
        "2.3.4",
        "MPL-2.0",
        "noarch",
        "A package for exercising basic features of RPM",
    )
    .epoch(1)
    .release("5")
    .description("This package attempts to exercise basic features of RPM packages.")
    .vendor("Los Pollos Hermanos")
    .url("http://www.savewalterwhite.com/")
    .vcs("https://github.com/rpm-rs/rpm")
    .group("Development/Tools")
    .packager("Walter White")
    .compression(CompressionType::None)
    .build_host("localhost")
    .source_date(1681068559)
    .provides(Dependency::any("/usr/bin/ls"))
    .provides(Dependency::any("aaronpaul"))
    .provides(Dependency::any("breaking(bad)"))
    .provides(Dependency::config("rpm-basic", "1:2.3.4-5.el9"))
    .provides(Dependency::eq("rpm-basic", "1:2.3.4-5.el9"))
    .provides(Dependency::eq("shock", "33"))
    .requires(Dependency::script_pre("/usr/sbin/ego"))
    .requires(Dependency::config("rpm-basic", "1:2.3.4-5.el9"))
    .requires(Dependency::greater_eq("methylamine", "1.0.0-1"))
    .requires(Dependency::less_eq("morality", "2"))
    .requires(Dependency::script_post("regret"))
    .conflicts(Dependency::greater("hank", "35"))
    .obsoletes(Dependency::less("gusfring", "32.1-0"))
    .obsoletes(Dependency::less("tucosalamanca", "444"))
    .supplements(Dependency::eq("comedy", "0:11.1-4"))
    .suggests(Dependency::any("chilipowder"))
    .enhances(Dependency::greater("purity", "9000"))
    .recommends(Dependency::any("SaulGoodman(CriminalLawyer)"))
    .recommends(Dependency::greater("huel", "9:11.0-0"))
    .with_file(
        "./tests/assets/SOURCES/example_config.toml",
        FileOptions::new("/etc/rpm-basic/example_config.toml").is_config(),
    )?
    .with_file(
        "./tests/assets/SOURCES/multiplication_tables.py",
        FileOptions::new("/usr/bin/rpm-basic"),
    )?
    .with_file(
        // I didn't bother filling all of these in yet, obviously it's not complete
        "",
        FileOptions::new("/usr/lib/rpm-basic").mode(FileMode::Dir { permissions: 0o644 }),
    )?
    .with_file(
        "",
        FileOptions::new("/usr/lib/rpm-basic/module").mode(FileMode::Dir { permissions: 0o755 }),
    )?
    .with_file(
        "./tests/assets/SOURCES/module/__init__.py",
        FileOptions::new("/usr/lib/rpm-basic/module/__init__.py"),
    )?
    .with_file(
        "./tests/assets/SOURCES/module/hello.py",
        FileOptions::new("/usr/lib/rpm-basic/module/hello.py"),
    )?
    .with_file(
        "",
        FileOptions::new("/usr/lib/rpm-basic/module").mode(FileMode::Dir { permissions: 0o755 }),
    )?
    .with_file(
        "",
        FileOptions::new("/usr/share/doc/rpm-basic").mode(FileMode::Regular { permissions: 0o644 }),
    )?
    .with_file(
        "",
        FileOptions::new("/usr/share/doc/rpm-basic/README").is_doc(),
    )?
    .with_file(
        "./tests/assets/SOURCES/example_data.xml",
        FileOptions::new("/usr/share/rpm-basic/example_data.xml"),
    )?
    .with_file(
        "",
        FileOptions::new("/var/log/rpm-basic/basic.log").is_ghost(),
    )?
    .with_file(
        "",
        FileOptions::new("/var/tmp/rpm-basic").mode(FileMode::Dir { permissions: 0o755 }),
    )?
    .add_changelog_entry(
        "Walter White <ww@savewalterwhite.com> - 3.3.3-3",
        "- I'm not in the meth business. I'm in the empire business.",
        1623672000,
    )
    .add_changelog_entry(
        "Gustavo Fring <gus@lospolloshermanos.com> - 2.2.2-2",
        "- Never Make The Same Mistake Twice.",
        1619352000,
    )
    .add_changelog_entry(
        "Mike Ehrmantraut <mike@lospolloshermanos.com> - 1.1.1-1",
        "- Just because you shot Jesse James, don't make you Jesse James.",
        1619352000,
    )
    .build()?;

You can do minor things like adding line breaks, but that's not the only issue. Say you wanted to build an RPM from JSON, or user input or something at runtime, and you need to intertwine the builder with loops and if statements. This can be done but you would need to constantly re-assign the variable e.g. #193 (comment)

What we could potentially do instead, is switch to pass-by-reference style. e.g.

let mut builder = PackageBuilder::new(
        "rpm-basic",
        "2.3.4",
        "MPL-2.0",
        "noarch",
        "A package for exercising basic features of RPM",
    );

builder
    .epoch(1)
    .release("5")
    .description("This package attempts to exercise basic features of RPM packages.")
    .vendor("Los Pollos Hermanos")
    .url("http://www.savewalterwhite.com/")
    .group("Development/Tools")
    .packager("Walter White");

if github_repo {
    builder.vcs("https://github.com/rpm-rs/rpm");
}

builder
    .provides(Dependency::any("/usr/bin/ls"))
    .provides(Dependency::any("aaronpaul"))
    .provides(Dependency::any("breaking(bad)"))
    .provides(Dependency::config("rpm-basic", "1:2.3.4-5.el9"))
    .provides(Dependency::eq("rpm-basic", "1:2.3.4-5.el9"))
    .provides(Dependency::eq("shock", "33"));

for req in requirements {
    builder.requires(req);
}

That would simplify intertwining logic into the builder construction.

@juliusl
Copy link
Contributor

juliusl commented May 6, 2024

I feel like this is what the fold/try_fold adapters are really good at.

For example, w/ pass-by-value that last example is just:

builder = requirements.fold(builder, |b, req| b.requires(req)); 

Similarly, for your other examples, it could be handled similarly,

// This can be created by reading in a file
let files = [
  ("./tests/assets/SOURCES/example_config.toml", FileOptions::new("/etc/rpm-basic/example_config.toml").is_config()),
  ("./tests/assets/SOURCES/multiplication_tables.py", FileOptions::new("/usr/bin/rpm-basic")),
  ("", FileOptions::new("/usr/lib/rpm-basic").mode(FileMode::Dir { permissions: 0o644 })),
  ("", FileOptions::new("/usr/lib/rpm-basic/module").mode(FileMode::Dir { permissions: 0o755 }))
];

builder = files.try_fold(builder, |b, (dest, opts)| {
  b.with_file(dest, opts)
})?;

@dralley
Copy link
Collaborator Author

dralley commented May 6, 2024

For example, w/ pass-by-value that last example is just:

builder = requirements.fold(builder, |b, req| b.requires(req));

Well, you say "just" but that's a lot of boilerplate that ultimately does not really need to be there.

If there's no particular benefit from passing by ownership in this context, then why sacrifice usability to do so?

@juliusl
Copy link
Contributor

juliusl commented May 14, 2024

For example, w/ pass-by-value that last example is just:

builder = requirements.fold(builder, |b, req| b.requires(req));

Well, you say "just" but that's a lot of boilerplate that ultimately does not really need to be there.

If there's no particular benefit from passing by ownership in this context, then why sacrifice usability to do so?

I think I see your point (I hope I didn't offend w/ my usage of just). Since it looks like in your example you're passing back &mut Self the existing builder pattern pretty much stays the same.

However, this last portion,

for req in requirements {
    builder.requires(req);
}

Seemed to conflict w/ the stated problem of and you need to intertwine the builder with loops and if statements.

I wanted to point out that since an iterator adapter exists, you could make this improvement today via an extension function that can reduce/fold state without needing to change the existing api.

And turn this into a 1 liner on the consumer side,

builder.requires(requirements);

@dralley
Copy link
Collaborator Author

dralley commented May 22, 2024

I don't see how it's a conflict, exactly. I agree that you can do it via iterator adapter, but I'm not certain that it's the most ergonomic way.

Another potential reason, though, is that I'd kinda like to expose Python bindings eventually and I think simple reference-based APIs would make that easier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants