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

Add macros to construct libcnb-data newtypes from literal strings #172

Merged
merged 10 commits into from
Nov 19, 2021

Conversation

Malax
Copy link
Member

@Malax Malax commented Nov 19, 2021

Problem Statement

With the use of newtypes to enforce correct values for CNB defined types also comes a drawback: we no longer cannot use string literals in places where the a literal value is needed. This forces the user to put .unwrap calls in these cases:

let process = Process::new(
    "web".parse().unwrap(),
    "bundle",
    vec!["exec", "ruby", "app.rb"],
    false,
    true,
);

Currently, this is not too bad as we only have this case with ProcessType really, usually once per buildpack if at all. But as soon as we implement #101, we will have this for every layer. I still think having newtypes for these scenarios is great but we need a solution for literal values and this is where this PR comes in.

Solution

This PR extends the libcnb_newtype! macro introduced in #162 so that it generates another macro for every newtype that allows us to create literal newtype values that are checked at compile time! The previous example becomes:

let process = Process::new(
    process_type!("web"),
    "bundle",
    vec!["exec", "ruby", "app.rb"],
    false,
    true,
);

Should the string not be a valid ProcessType, the buildpack will not compile:

Compiling example-02-ruby-sample v0.1.0 (/Users/manuel.fuchs/projects/libcnb.rs/libcnb/examples/example-02-ruby-sample)
error: "wörker" is not a valid ProcessType value!
  --> libcnb/examples/example-02-ruby-sample/src/main.rs:56:25
   |
56 |                         process_type!("wörker"),
   |                         ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in the macro `process_type` (in Nightly builds, run with -Z macro-backtrace for more info)

Since these are regular compile errors, the error nicely propagates into IDEs out of the box:

image

Implementation Notes

Regex

The implementation is very generic since we can validate all our newtype values via regular expressions. This allows us to compile the regular expression at compile time and use it against the literal string passed to the macro. As soon as we need to use arbitrary Rust code to validate these, the current approach with automatically generated macros is no longer feasible, unless we write a Rust interpreter or use a scripting language for the validation logic.

To extend the cases where regular expressions can be used, I replaced the regex crate with fancy_regex to allow for look-around expression which cannot be implemented in a NFA regex implementation. This means our regular expressions are no longer guaranteed to run in linear time. I think this is a fine trade-off for us, since we're only using it with very simple expressions that are entirely controlled by us and only used in a very limited use-case.

See: Catastrophic Backtracking

Removal of extra predicates

See above, we cannot use arbitrary Rust code to do the validation at compile time since we would have no way to execute it. We can implement allow- and blocklists via regex just fine and don't need extra predicates at this point.

New crate: libcnb-proc-macros

This feature can only be implemented via procedural macros. By design, procedural macros cannot be used in the crate they're defined it and need a special crate type. Therefore, we have a new crate for the proc macro required by this PR and potential future ones.

Testing Strategy

Testing code that, by design, should not compile is hard. Rust currently provides no way to test non-compiling code in unit tests. This limits the tests to only test the positive cases. I would like to test the negative code path as well. There is compiletest-rs which seems overkill in our-case.

To keep things simpler, I'd fine to not test the negative path for now. Note that the string will still be validated at runtime as well, the worst-case scenario would be that code with an illegal newtype value compiles, but fails at runtime as if these macros would not exist. I'm happy to be convinced otherwise. :)

GUS-W-10203923.

@Malax Malax force-pushed the malax/newtype-literals-macro branch 3 times, most recently from 9bb7556 to 3a06ec3 Compare November 19, 2021 15:39
@Malax Malax force-pushed the malax/newtype-literals-macro branch from 3a06ec3 to 666f14a Compare November 19, 2021 15:41
@Malax Malax marked this pull request as ready for review November 19, 2021 15:44
@Malax Malax requested review from edmorley and schneems November 19, 2021 15:44
libcnb-proc-macros/Cargo.toml Show resolved Hide resolved
libcnb-data/src/buildpack.rs Outdated Show resolved Hide resolved
libcnb-data/src/buildpack.rs Outdated Show resolved Hide resolved
libcnb-data/src/newtypes.rs Outdated Show resolved Hide resolved
libcnb-proc-macros/src/lib.rs Show resolved Hide resolved
libcnb-data/src/newtypes.rs Outdated Show resolved Hide resolved
@edmorley
Copy link
Member

Thank you for doing this! I much prefer the idea of compile time validation :-)

@Malax Malax merged commit 9c6843f into main Nov 19, 2021
@Malax Malax deleted the malax/newtype-literals-macro branch November 19, 2021 17:41
@edmorley edmorley added this to the 0.4.0 milestone Dec 8, 2021
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

Successfully merging this pull request may close these issues.

2 participants