-
Notifications
You must be signed in to change notification settings - Fork 86
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
feat(extension): add support for custom canister types #3222
Conversation
Question on this, if someone is using a custom type like |
Thanks for bringing that up; it definitely should work just as you've described it, I'll make a test case for it |
7124055
to
65e36a7
Compare
65e36a7
to
1e46751
Compare
let extension_manager = | ||
crate::extension::manager::ExtensionManager::new(dfx_version).unwrap(); | ||
custom_canister_type::transform_dfx_json_via_extension(&mut json, extension_manager) | ||
.unwrap(); // TODO: error handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what kind of error to use over here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since from_slice will now have new failure modes, we'll have to change the return type.
We can reduce the scope of those failure modes, though.
How about this?
- Instantiate the ExtensionManager in the caller
- make EnvironmentImpl::new() create the ExtensionManager.
- rename EnvironmentImpl::new_extension_manager()` and
- Pass a trait rather than the whole ExtensionManager
- make
trait TransformConfiguration
- single method:
transform(&mut json) -> Result<(), TransformConfigurationError>
- either pass
Option<TransformConfiguration>
to these methods, or for tests pass an object that is noop for this method
- single method:
- Change these methods' error type from StructuredFileError to ReadConfigurationError (possibilities: StructuredFileError, TransformConfigurationError)
This way the error return type of from_slice
and friends will reflect the errors that can occur, but won't also have to include errors related to instantiating the extension manager.
// Override custom canister declaration values by the real canister_declaration | ||
for (key, value) in values.iter() { | ||
if key != "type" && key != "canister_name" { | ||
final_fields.insert(key.clone(), value.clone()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ericswanson-dfinity I just realized this is buggy... I'm working on a fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After all, I'm not sure what should be the desired behavior...
I extended unit tests with:
#[test]
fn test_op_replace_basic() {
test_op!(
custom_canister_template = r#"
{
"main": { "replace": { "input": "{{canister_name}}", "search": "frontend_(.*)", "output": "thecanister/$1/main.ts" } }
}
"#,
dfx_json_canister_values = r#"
{
"canister_name": "frontend_xyz"
}
"#,
expected = r#"
{
"main": "thecanister/xyz/main.ts"
}
"#
);
}
#[test]
fn test_op_replace_nested() {
test_op!(
custom_canister_template = r#"
{
"main": { "replace": { "input": "{{main}}", "search": "(.*)", "output": "thecanister/$1" } }
}
"#,
dfx_json_canister_values = r#"
{
"main": "src/main.ts"
}
"#,
expected = r#"
{
"main": "thecanister/src/main.ts"
}
"#
);
}
the _basic
will pass, but what should be the expected outcome for _nested
? This is related to #3222 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how this is related to the comment.
From reading the comment I think the expectation is that
{
"type": "kybra",
"gzip": "true"
}
should transform according to the rules of the kybra extension, but also keep the "gzip" field.
Also I think these tests would benefit from some prose descriptions of what's being tested, what's the expected behavior, and what are the nuances. As a reader I don't want to have to parse a regex in my head in order to figure out what the test is checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apologies, I mixed too much info in that example; let me untangle. Going from the simplest to the most complex:
-
Append user-defined field: This is when a user adds a field, like
gzip: true
, to theirdfx.json
file, despitegzip
not being defined in the canister template. This currently works as expected.#[test] fn test_op_replacement_1() { test_op!( custom_canister_template = r#" { "main": "something.py" } "#, dfx_json_canister_values = r#" { "gzip": true } "#, expected = r#" { "gzip": true, "main": "something.py" } "# ); }
-
Override the field: This is when a field such as
gzip: false
is defined in the canister template but the user modifies its value in theirdfx.json
, likegzip: true
. This modification also works as expected.#[test] fn test_op_replacement_2() { test_op!( custom_canister_template = r#" { "main": "something.py", "gzip": false } "#, dfx_json_canister_values = r#" { "gzip": true } "#, expected = r#" { "gzip": true, "main": "something.py" } "# ); }
-
Nested field override, while using the field as a template for another field: This case happens when a field defined in the canister template is overridden by the user in their
dfx.json
, and the initial value of this field is employed as a placeholder in another field in the template. This function is also performing as expected for now.#[test] fn test_op_replacement_3() { test_op!( custom_canister_template = r#" { "main": "{{gzip}}.py", "gzip": false } "#, dfx_json_canister_values = r#" { "gzip": true } "#, expected = r#" { "gzip": true, "main": "true.py" } "# ); }
-
Nested field override, using the field's original value as a placeholder for the same field: This is the scenario when a field's value in the canister template is both overridden by the user in their
dfx.json
and used as a placeholder within the same field. For instance, ifmain
in the template ismain: "path/to/{{main}}"
, and the user hasmain: "something.py"
indfx.json
, the expected result should bemain: "path/to/something.py"
. However, the present tests for this case are failing.#[test] fn test_op_replacement_4() { test_op!( custom_canister_template = r#" { "main": "path/to/{{main}}" } "#, dfx_json_canister_values = r#" { "main": "something.py" } "#, expected = r#" { "main": "path/to/something.py" } "# ); }
---- extension::manifest::custom_canister_type::tests::test_op_replacement_4 stdout ---- thread 'extension::manifest::custom_canister_type::tests::test_op_replacement_4' panicked at 'assertion failed: `(left == right)` left: `{"main": String("something.py")}`, right: `{"main": String("path/to/something.py")}`', src/dfx-core/src/extension/manifest/custom_canister_type.rs:307:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
Nested field override with a replacement operation: This case involves a user-defined replacement operation in the
dfx.json
, where the value for the "main" field is used as a placeholder. This value gets replaced through a regex operation defined in the canister template. For example, ifmain
in the template is"main": { "replace": { "input": "{{main}}", "search": ".*/(.*).ts", "output": "thecanister/$1.exe" } }
, and the user hasmain: "src/main.ts"
indfx.json
, the expected result should bemain: "thecanister/main.exe"
. Currently, the tests for this scenario are also failing.#[test] fn test_op_replacement_5() { test_op!( custom_canister_template = r#" { "main": { "replace": { "input": "{{main}}", "search": ".*/(.*).ts", "output": "thecanister/$1.exe" } } } "#, dfx_json_canister_values = r#" { "main": "src/main.ts" } "#, expected = r#" { "main": "thecanister/main.exe" } "# ); }
---- extension::manifest::custom_canister_type::tests::test_op_replacement_5 stdout ---- thread 'extension::manifest::custom_canister_type::tests::test_op_replacement_5' panicked at 'assertion failed: `(left == right)` left: `{"main": String("src/main.ts")}`, right: `{"main": String("thecanister/main.exe")}`', src/dfx- core/src/extension/manifest/custom_canister_type.rs:328:9
let me know if you agree all of these should pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, these are great explanations. I think it would be helpful to include these explanations as comments in the tests themselves. What do you think?
As for the scenarios:
- agree
- agree
- agree, though I wonder: for a field like this where the type template is using one field as input to generate another field, wouldn't it be common for the template to include an op to remove the input field? Also,
"gzip"
might be a counterintuitive name for the field that demonstrates this behavior. - I would expect this test to pass as written
- I would expect this test to pass as written
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, it's good that we established this baseline. Yes, adding these comments to the code seems like a good idea. I agree that using gzip
in example #3 is counterintuitive.
Adding remove: true
adds another layer of complexity, which I think should be discussed separately.
I laid out these tests like one in the previous post; hope that's easy enough to follow.
- Remove the field, if it was defined in
dfx.json
#[test] fn test_op_replace_and_delete_1() { test_op!( custom_canister_template = r#" { "main": "something.py", "gzip": { "remove": true } } "#, dfx_json_canister_values = r#" { "gzip": true } "#, expected = r#" { "main": "something.py" } "# ); }
- Disallow overwriting the field?
#[test] fn test_op_replace_and_delete_2() { test_op!( custom_canister_template = r#" { "main": "something.py", "gzip": false, "gzip": { "remove": true } } "#, dfx_json_canister_values = r#" { "gzip": true } "#, expected = r#" { "gzip": false, "main": "something.py" } "# ); }
- unclear what the expected behavior should be here
#[test] fn test_op_replace_and_delete_3() { test_op!( custom_canister_template = r#" { "main": "{{gzip}}.py", "gzip": false, "gzip": { "remove": true } } "#, dfx_json_canister_values = r#" { "typ": true } "#, expected = r#" { "gzip": false, "main": "true.py" } "# ); }
- unclear what the expected behavior should be here
#[test] fn test_op_replace_and_delete_4() { test_op!( custom_canister_template = r#" { "main": "path/to/{{main}}", "main": { "remove": true } } "#, dfx_json_canister_values = r#" { "main": "something.py" } "#, expected = r#" { "main": "path/to/something.py" } "# ); }
- unclear what the expected behavior should be here
#[test] fn test_op_replace_and_delete_5() { test_op!( custom_canister_template = r#" { "main": { "replace": { "input": "{{main}}", "search": ".*/(.*).ts", "output": "thecanister/$1.exe" } }, "main": { "remove": true } } "#, dfx_json_canister_values = r#" { "main": "src/main.ts" } "#, expected = r#" { "main": "thecanister/main.exe" } "# ); }
- unclear what the expected behavior should be here
#[test] fn test_op_replace_and_delete_6() { test_op!( custom_canister_template = r#" { "main": { "replace": { "input": "{{canister_name}}", "search": "frontend_(.*)", "output": "thecanister/$1/main.ts" } } } "#, dfx_json_canister_values = r#" { "canister_name": "frontend_xyz" } "#, expected = r#" { "main": "thecanister/xyz/main.ts" } "# ); }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's start with documentation in docs/concepts describing how to define an custom canister type and how to use one in your project. This will help with the review for questions like "what is "type": "<x>:<y>"
97ef832
to
518c32d
Compare
0fb1740
to
984524c
Compare
Replaced by #3641 |
Description
The PR extends functionalities to adapt to the new feature of custom canister types. The changes accommodate:
canister_types
in the extension manifest file.canisters.<CAN>
in dfx.json based on extension'scanister_types
declarationNote:
Closes https://dfinity.atlassian.net/browse/SDK-1073
How Has This Been Tested?
Checklist: