ealush / vest

Vest ✅ Declarative validations framework
https://vestjs.dev/
MIT License
2.57k stars 85 forks source link

Support for nested suites #1141

Open codrin-iftimie opened 8 months ago

codrin-iftimie commented 8 months ago

I'm researching libraries that can be used to replace my in-house form validator library. I'm using generators at the moment. Most of the requirements this library checks them. Kudos for that. But, one of the common requirements is the ability to nest validators to ensure good code reusability. Let's take this as an example

const itemSuite = create(() => {
  test('field1', 'Field1 is invalid' () => {
    //
  })
})

const mainSuite = create((items) => {
  test('items', 'One or more item is invalid' () => {
    ensure(data.items).matchesArrayOfSuite(itemSuite)
  })

  test('innerItem', 'This inner item is not valid', () => {
    ensure(data.innerItem).matchesSuite(itemSuite)
  })
})

In some cases, I'd like to use the itemSuite to validate an item object on a "details" page where there are independent CRUD operations for each item. In other cases, I have a wizard where we allow the user to add these items as part of a bigger payload (see mainSuite). I know that matchesArrayOfSuite nor matchesSuite is not part of the current enforce API. Is there any plan to support something similar to this that would allow nesting suites? Expectations would be to see a general error on items and innerItem if the object is not valid, but also have mainSuite to inherit the details from itemSuite to show them to their respective fields.

codrin-iftimie commented 8 months ago

Seems that the following is producing the right result

const itemSuite = create("itemSuite", (item, path) => {
  test(`${path}.name`, "Name is required", () => {
    enforce(item.name).isNotEmpty();
  });
});

const mainSuite = create("mainSuite", (data, field) => {
  only(field);

  test("a", "A != B", () => {
    enforce(data.a).equals(data.b);
  });

  itemSuite(data.innerItem, "innerItem");

  each(data.items, (item) => {
    itemSuite(item, `items.${item.guid}`);
  });
});

const result = mainSuite(
  {
    a: "a",
    b: "b",
    items: [
      { name: "", guid: 1 },
      { name: "", guid: 2 },
      { name: "test", guid: 3 },
    ],
    innerItem: {
      name: "",
    },
  },
  ["items.1.name", "innerItem.name"]
); // result.errorCount = 2

Would you advise devs to use this library this way?

ealush commented 8 months ago

Hey @codrin-iftimie, sorry for responding late. I have been traveling.

Yes, your approach should work as expected, and most operations should work correctly when nesting suites this way.

only, omitWhen, and skipWhen, include should work with no problem. Querying and outputting these results should work as well.

By the way, this will work just the same if you simply use a function for your nested suite, and do not create an entire suite for that. Meaning, simply the callback of your sub suite is sufficient for this. It will be a little easier on performance.

In the future, I might add a few more features around the approach that you selected (creating two suites), but for now, these two approaches are identical in terms of functionality.