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

Unify union types in the same error #17

Merged
merged 2 commits into from
Jun 2, 2020
Merged

Unify union types in the same error #17

merged 2 commits into from
Jun 2, 2020

Conversation

gillchristian
Copy link
Owner

@gillchristian gillchristian commented Dec 22, 2019

Improves on #5

I started to work on the reporting of union types errors. It's not yet finished, but I'd like some feedback on the code (I think my editor applied automatic formatting btw 🤦‍♂) and also on how the errors look.

Here are some examples one the errors, and what's still missing.

t.keyof

io-ts suggests to use t.keyof for union types of only string literals.

const OneOf = t.interface({
    oneOf: t.keyof({ a: null, b: null, c: null }),
});

report(OneOf.decode({ oneOf: '' }));

That still reports as before:

Expecting "a" | "b" | "c" at oneOf but instead got: "A".

Literals unions (using t.union):

const StringUnion = t.interface({
    stringUnion: t.union([t.literal('a'), t.literal('b'), t.literal('c')]),
});
report(StringUnion.decode({ stringUnion: 'A' }));

Reports with the new format (instead of showing 3 different errors):

Expecting one of:
    "a"
    "b"
    "c"
at stringUnion but instead got: "A".

Things to improve:

There are some things that should still be improved. For unions of other types, I need to do some improvements on the usage of the context to get proper errors.

const Unions = t.interface({
    interfaceUnion: t.union([
        t.interface({ key: t.string }),
        t.interface({ code: t.number }),
    ]),
});

For such union, the following input would give the error one expects:

report(Unions.decode({ interfaceUnion: 'name' }));
Expecting one of:
    { key: string }
    { code: number }
at interfaceUnion but instead got: "name".

But for the case when there's more context of the value:

report(Unions.decode({ interfaceUnion: {} }));

The error is wrong 😕

Expecting one of:
    string
    number
at interfaceUnion but instead got: undefined.

@gillchristian
Copy link
Owner Author

As a side note, I think it would be beneficial to add assertions to the tests, since changes such as this PR would require several cases being checked.

Also I could add prettier (to prevent what happened with my changes).

Didn't want to go ahead and add such stuff since it's not my call 😅 But I'm open to make such additions if you think it's beneficial.

@OliverJAsh
Copy link
Collaborator

OliverJAsh commented Feb 4, 2020

Hey @gillchristian, really sorry for taking so long to get back to you on this.

I'm not using io-ts at the moment and I'm finding it really hard to remember how everything fits together. It might be better if I just added you add a contributor to this repo, or you could maintain a fork (I'd be happy to link from my repo to yours)?

@gillchristian
Copy link
Owner Author

No worries. I parked the work on this PR from my side anyways 😴

If you are fine with having a contributor I'd prefer that since a fork would be misleading. I'm also happy to maintain the project in general, since it's already the one listed in fp-ts / io-ts websites better to keep this one alive, wdyt?

@OliverJAsh
Copy link
Collaborator

@gillchristian I've sent you an invite :-)

@gillchristian
Copy link
Owner Author

Finally put some time into this. The errors look decent now.

There could be some more work into making the formatting smart: eg. if the provided value is "closer" to the type of one of the branches of the union, then only report to that one. But for now is good enough I guess.

@gillchristian gillchristian changed the title [WIP] Unify union types in the same error Unify union types in the same error Mar 7, 2020
@osdiab
Copy link
Contributor

osdiab commented May 20, 2020

I made a PR on your repo that merges in the current master and converts your tests to the new test format FYI!

src/index.ts Outdated
: O.some(
// https://github.com/elm-lang/core/blob/18c9e84e975ed22649888bfad15d1efdb0128ab2/src/Native/Json.js#L199
// tslint:disable-next-line:prefer-template
`Expecting one of:\n${expected}` +
Copy link
Contributor

@osdiab osdiab May 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing I'd change with your PR is that now there's two different places where the same error message is being constructed, either find a way for both code paths to merge, or extract the error message code to its own helper

my use case is that in supporting #25 , this function now doesn't have a specific error object associated with it, so it doesn't make sense what error message to show if multiple are present in the union case - for now in my fork i'm just not showing errors if it's a union error for simplicity, but ideally if there's one "dominant" error like mentioned in #5 to make it choose a branch smartly, that one would probably be the error message I'd display.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that and having two places to construct the same message seems like a likely way for them to go out of sync and introduce regressions, etc.

if my error message PR doesn't go through and this reporter chooses not to show error messages, then this particular problem isn't important, though not suppressing error messages seems like a good thing to me

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but ideally if there's one "dominant" error like mentioned in #5 to make it choose a branch smartly, that one would probably be the error message I'd display.

Totally agree on that.

And yes, we should unify the way error messages are generated.

@osdiab
Copy link
Contributor

osdiab commented May 20, 2020

FYI: I haven't looked into the code to figure out why, but I tried this branch decoding a codec of mine with some garbage data, and got a lot of duplicate values for the union:

// codec looks roughly like this
export const orgData = t.intersection([
  t.type({
    // ... a bunch of other entries including several union types
    revenueAmt: t.union([NumberFromString.pipe(BoundedInt), t.null]) 
  }),
    t.type({ id: t.string }),
    t.partial({
      customerInfo: customerInfoCodec // also a fairly complex type
    })
])

Result

Expecting one of:
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
    pipe(NumberFromString, BoundedInt)
    null
at revenueAmt but instead got: "5.24".

@gillchristian
Copy link
Owner Author

gillchristian commented May 20, 2020

@osdiab I think that is because it adds errors for every branch or something like that.

I need to get this PR in better shape, I'll take a look at your changes on the tests and pick up the work I was doing.

P.S I'll do some work on this and ping you

Comment on lines 141 to 156
reporter(
Person.decode({
name: 'Jane',
gender: 'female',
children: [{ gender: 'Whatever' }]
})
),
[
'Expecting number at age but instead got: undefined',
[
'Expecting one of:',
' "Male"',
' "Female"',
' "Other"',
'at gender but instead got: "female"'
].join('\n'),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@osdiab I think this case covers the error you pointed out on #17 (comment)

Expected:

Expecting one of:
    "Male"
    "Female"
    "Other"
at gender but instead got: "female"

But now it outputs:

Expecting one of:
    "Male"
    "Female"
    "Other"
    "Male"
    "Female"
    "Other"
at gender but instead got: "female"

Note that I only added the failing tests for now. I still need to work on the code.

@osdiab
Copy link
Contributor

osdiab commented May 29, 2020

any way I can help move this along? I'd love to get this into my prod codebase :) I can take a look in a couple days and make some changes if you're too busy to sink time into it.

@gillchristian
Copy link
Owner Author

I'll do some progress on Sunday & Monday (holiday here) and push my progress in case I do not finish. Wdyt?

@osdiab
Copy link
Contributor

osdiab commented May 29, 2020

works for me, no pressure! just excited to see it go in, it would make error logs at my company so much more readable haha

@gillchristian
Copy link
Owner Author

it would make error logs at my company so much more readable haha

Same reason why I started this PR :)

@gillchristian
Copy link
Owner Author

@osdiab I guess it should be good to go know.

We certainly can iterate on improvements (eg. only show the error from the type of the union that is closer). But for now I think it's fine like this.

Please check the tests to see how errors are formatted in different cases

Copy link
Contributor

@osdiab osdiab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test cases look good to me, and we can add more and refine iteratively :) thanks for the great PR!

import { Predicate } from 'fp-ts/lib/function';

/* eslint-disable @typescript-eslint/array-type */
export const takeUntil = <A = unknown>(predicate: Predicate<A>) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If implemented a find function this can be a one-liner btw, ES6 equivalent as.slice(0, as.findIndex(predicate) + 1) - but since find isn't available in all runtimes would need to implement it

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes find could work as well.

I took a quick look at how takeWhile is implemented in fp-ts and decided to go with that approach.

@gillchristian gillchristian merged commit 9792b5a into gillchristian:master Jun 2, 2020
@gillchristian gillchristian deleted the feature/union-types-formatting branch June 2, 2020 07:51
const jsToString = (value: t.mixed) =>
value === undefined ? 'undefined' : JSON.stringify(value);

export const formatValidationError = (error: t.ValidationError) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch. This was a breaking change for me :D

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh sorry! I'll find a way to fix it today.

Now there's a version for union errors and "other" errors. Because errors on union types are basically treated as one error per case of the union type so what I did was group by the path of the property that had an error. I'll look into a way of generalizing that and if it's not possible I might have to export both functions.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check #33

I also added another exported function that can be used to format the errors 'in place'

import * as t from 'io-ts';
import { formatValidationErrors } from 'io-ts-reporters';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';

const User = t.interface({ name: t.string });
pipe({ nam: 'Jane' }, User.decode, E.mapLeft(formatValidationErrors));

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

Successfully merging this pull request may close these issues.

None yet

4 participants