Fixes [#1239]#1240
Fixes [#1239]#1240Marsup merged 15 commits intohapijs:masterfrom samueljoli:1239-rename-key-using-regex
Conversation
lib/types/object/index.js
Outdated
| delete target[rename.to]; | ||
| } | ||
| else if (typeof rename.from === 'object') { | ||
| const fromKey = Object.keys(target).find((key) => { |
There was a problem hiding this comment.
Should not test if the object is an actual regex?
There was a problem hiding this comment.
@AdriVanHoudt comment addressed. Good catch
lib/types/object/index.js
Outdated
| if (target[rename.from] === undefined) { | ||
|
|
||
|
|
||
| if (target[rename.from] === undefined && !(rename.from instanceof RegExp)) { |
There was a problem hiding this comment.
We call rename.from instanceof RegExp 3 times in here seems like a good case to use a boolean and just reference that. ie. let isRegExRename = rename.from instanceof RegExp
DavidTPate
left a comment
There was a problem hiding this comment.
Can you also add some a piece to the docs about this new feature please? Just a short sentence and example will suffice.
lib/types/object/index.js
Outdated
| delete target[rename.to]; | ||
| } | ||
| else if (rename.from instanceof RegExp) { | ||
| const fromKey = Object.keys(target).find((key) => { |
There was a problem hiding this comment.
Can we loop over this with a more traditional method instead? There's a heavy cost to using the closure here in the critical path of the validation which would be elevated through not using it.
const objectKeys = Object.keys(target);
for (var i = 0, il = objectKeys.length; i < il; i++) {
...
}
lib/types/object/index.js
Outdated
| return rename.from.test(key); | ||
| }); | ||
|
|
||
| target[rename.to] = target[fromKey]; |
There was a problem hiding this comment.
We're going to have some weird behavior here if we encounter more than one matching key. I would think we either want to error when we encounter multiple keys matching the RegExp or convert each one of them. Currently, we will only rename the last matching one that we encounter.
Ideally, something like this:
const objectKeys = Object.keys(target);
for (var i = 0, il = objectKeys.length; i < il; i++) {
if (rename.from.test(objectKeys[i]) {
target[rename.to] = target[objectKeys[i]];
if (!rename.options.override) {
// don't override the key if it already exists (so validation would fail I believe)
}
if (!rename.options.alias) {
delete target[objectKeys[i]];
}
}
}
|
@DavidTPate Comments addressed |
lib/types/object/index.js
Outdated
| if (rename.from.test(objectKeys[j])) { | ||
| target[rename.to] = target[objectKeys[j]]; | ||
|
|
||
| if (!rename.options.override) { |
There was a problem hiding this comment.
From what I see in the docs this is not the correct usage of override.
override- iftrue, allows renaming a key over an existing key. Defaults tofalse.
Instead, this particular usage should be alias.
alias- iftrue, does not delete the old key name, keeping both the new and old keys in place. Defaults tofalse.
So this particular line should be if (!rename.options.alias) {
and then we also need to implement override for the regular expressions as well.
You can see how we implement that here.
test/types/object.js
Outdated
| const schema = Joi.object({ | ||
| fooBar: Joi.string(), | ||
| fooBaz: Joi.string() | ||
| }).rename(regex, 'fooBar', { override: true }); |
There was a problem hiding this comment.
Once we fix the usage of alias we should update this test and also add a test for the override functionality.
I would expect that if we have multiple keys matching our regex and override set to false then we would throw an error. Similarly if we have override set to false and the property is already on the object we would throw an error.
|
@DavidTPate Comments addressed. Things to note: There was a question that arose when implementing If we're using regex, than the schema / target that we are validating against very well could have a key that is in a different case compared to how it's defined in the joi schema. So I added some logic to normalize the key in question back to the case form that it is defined in the schema. I've added comments. |
lib/types/object/index.js
Outdated
|
|
||
| if (target[rename.from] === undefined) { | ||
|
|
||
| const isRegExRename = rename.from instanceof RegExp; |
There was a problem hiding this comment.
Just store it when you're calling the rule, no need to check that on each validation.
lib/types/object/index.js
Outdated
|
|
||
| // Using regex: Key needs to be normalized to proper case | ||
|
|
||
| for (let k = this._inner.children.length; k--;) { |
There was a problem hiding this comment.
This part is wrong, rename has ato argument, use that, plain and simple, don't try to be clever by looking for the matching child. And also make use of the options, some cases are missing here.
|
@Marsup - @samueljoli and I did a pretty big re-write of his implementation to simplify it. We also added several more tests and error handling around multiple matching keys with |
Marsup
left a comment
There was a problem hiding this comment.
I'll just stick to that one comment and let you update before continuing the review, I think it makes more sense that I review once you've fixed that.
lib/types/object/index.js
Outdated
|
|
||
| if (target[rename.from] === undefined) { | ||
| delete target[rename.to]; | ||
| if (rename.isRegExp) { |
There was a problem hiding this comment.
The place of this if is likely wrong, it should be a regexp path or a string path, trying to use both is madness. For example you were just lucky in your ignoreUndefined test cases :
const schema = Joi.object({
b: Joi.any()
}).rename(/^a$/, 'b', { ignoreUndefined: true });
schema.validate({ a: 1 }) // Errors because it failed to rename a to b, it stopped the loop in the 1st statementThere was a problem hiding this comment.
Hey alright so we addressed that test case, but I honestly have mixed feelings on the implementation for the fix. We are using a .filter to do the undefined check and also prevent a duplicate future iteration as a side effect of the filter callback. Don't like that, but it does prevent double iteration over the same keys.
Let us know what you think of this approach. We can also just split the regexp and string paths completely; that just meant duplicating the exact logic for the multiple and override checks into both blocks.
Marsup
left a comment
There was a problem hiding this comment.
I still think trying to insert the regexp case into the normal case is wrong, the more I look at the code the more it seems overly complex.
lib/types/object/index.js
Outdated
| const rename = this._inner.renames[i]; | ||
| const matchedTargetKeys = []; | ||
|
|
||
| const fromIsUndefined = rename.isRegExp ? |
There was a problem hiding this comment.
The semantic is bit different, ignoreUndefined is supposed to ignore, well, undefined :)
Finding that it's a match and that it's undefined is quite different.
There was a problem hiding this comment.
Agreed. We will refactor to completely separate the string/regex paths. That should clean up this bit too.
lib/types/object/index.js
Outdated
| if (!rename.options.multiple && | ||
| matchedTargetKeys.length > 1) { | ||
|
|
||
| errors.push(this.createError('object.rename.multiple', { from: matchedTargetKeys, to: rename.to }, state, options)); |
There was a problem hiding this comment.
So in theory, if abortEarly is false, you could get two object.rename.multiple for a single rename, that seems weird.
There was a problem hiding this comment.
That would be the desired result when the target object has two keys that both match the rename from regex, right? I agree it's weird, but I don't know what the other option would be.
There was a problem hiding this comment.
Probably not, your current code is, I think, giving a single error containing all the fields that couldn't be renamed. The problem here is it's erroring on the case that was previously for strings, and also on this line that is made for regexps.
There was a problem hiding this comment.
Oh I see what you mean now. Not "2 errors" as in the 2 fields, but literally hitting two different error blocks in the code for the same rename object. 👍
I think completely separating the regex/string code paths will solve that.
lib/types/object/index.js
Outdated
| const matchedTargetKeys = []; | ||
|
|
||
| const fromIsUndefined = rename.isRegExp ? | ||
| Object.keys(target).filter((targetKey) => { |
There was a problem hiding this comment.
Instead of using Array.filter here, can we switch this to utilizing a plain old for.... We usually try to steer clear of Array.filter, Array.map, etc. within the critical path of code due to the performance cost of them.
There was a problem hiding this comment.
Yeah, based on @Marsup's feedback above this whole block will be re-written and we won't need the filter anymore anyways :)
|
This thread has been automatically locked due to inactivity. Please open a new issue for related bugs or questions following the new issue template instructions. |
This PR extends
Joi.object.rename()to allow the renaming of key properties via regex. The necessity arose from needing to validate against url query parameters and being case insensitive, while still being able to refer to the param in question in my src code in it's default, expected format.Use case:
client requests...
base_url/resource?marketCode=191- camel casedbase_url/resource?marketcode=191- lower casedbase_url/resource?MarketCode=191- pascal cased