-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Fixes [#1239] #1240
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
Fixes [#1239] #1240
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not test if the object is an actual regex?
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.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
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.
Looks good to me!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -118,16 +119,42 @@ internals.Object = class extends Any { | |||
} | |||
} | |||
|
|||
if (target[rename.from] === undefined) { | |||
|
|||
const isRegExRename = rename.from instanceof RegExp; |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
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'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
@@ -118,18 +119,53 @@ internals.Object = class extends Any { | |||
} | |||
} | |||
|
|||
if (target[rename.from] === undefined) { | |||
delete target[rename.to]; | |||
if (rename.isRegExp) { |
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.
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 statement
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.
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.
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 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
@@ -94,8 +95,22 @@ internals.Object = class extends Any { | |||
const renamed = {}; | |||
for (let i = 0; i < this._inner.renames.length; ++i) { | |||
const rename = this._inner.renames[i]; | |||
const matchedTargetKeys = []; | |||
|
|||
const fromIsUndefined = rename.isRegExp ? |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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