Skip to content

RFE: Better extensibility #73

@arv

Description

@arv

We are building some features on top of Valita and with the .4 release a lot of the public APIs that we depend on are now marked as private. I'm going to outline the things we are using to see if we can figure out how to expose these in a clean way.

Command Line Parser

We have a library that takes a Valita schema and generates a command line parser.

The code is ~500 LOC and it will be open sourced this year. I can give access to it early if you are curious.

API Used

toTerminals: This is used for the visitor pattern. We use it to gather the literals and the types.

ArrayType.prefix, ArrayType.rest, ArrayType.suffix: toTerminals was not called on these so we had to do that manually. This is used with the above. I believe we can mostly create our own visitor except for these which are not exposed.

func: We used this to get the default value out of an optional. I think I can test for name === 'optiopnal' and in that case there is no default value. Otherwise I can call schema.parse(undefined) and see what I get out of it.

Customized Error Messages

No breaking changes here but it does reproduce some of the functionality (~100 LOC). Not a big deal at the moment.

Better Union Error Reporting

When a union fails to parse we find the longest partial match and show the error for that. This allows errors like

API Used

  • UnionType.options: This is used to get the different types of the union and we try all of them to see which one gives use the longest path.
Sample Implementation
type FailedType = {type: v.Type; err: v.Err};

function getDeepestUnionParseError(
  value: unknown,
  schema: v.UnionType,
  mode: ParseOptionsMode,
): string {
  const failures: FailedType[] = [];
  for (const type of schema.options) {
    const r = type.try(value, {mode});
    if (!r.ok) {
      failures.push({type, err: r});
    }
  }
  if (failures.length) {
    // compare the first and second longest-path errors
    failures.sort(pathCmp);
    if (failures.length === 1 || pathCmp(failures[0], failures[1]) < 0) {
      return getMessage(failures[0].err, value, failures[0].type, mode);
    }
  }
  // paths are equivalent
  try {
    const str = JSON.stringify(value);
    return `Invalid union value: ${str}`;
  } catch (e) {
    // fallback if the value could not be stringified
    return `Invalid union value`;
  }
}


// Descending-order comparison of Issue paths.
// * [1, 'a'] sorts before [1]
// * [1] sorts before [0]  (i.e. errors later in the tuple sort before earlier errors)
function pathCmp(a: FailedType, b: FailedType) {
  const aPath = a.err.issues[0].path;
  const bPath = b.err.issues[0].path;
  if (aPath.length !== bPath.length) {
    return bPath.length - aPath.length;
  }
  for (let i = 0; i < aPath.length; i++) {
    if (bPath[i] > aPath[i]) {
      return -1;
    }
    if (bPath[i] < aPath[i]) {
      return 1;
    }
  }
  return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions