Skip to content

More Structured format for parameters and Enhanced FES #7843

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

Open
wants to merge 1 commit into
base: dev-2.0
Choose a base branch
from

Conversation

IIITM-Jay
Copy link
Contributor

@IIITM-Jay IIITM-Jay commented May 24, 2025

Resolves #7752

Overall Description

This PR deals with generating more structured and informative parameterData.json in order to get a clear and accurate idea of the various parameters involved in overloads of methods. Additionally, as a part of the enhancement process, this PR also resolves and enhances the ability to check the types of the nested as well as positional parameters efficiently at run time, which is also one of the critical aspect of the issue.

Approach Used

  • Enhancements made to be able to build more structural format to parameterData.json

    • The parameters of FunctionType node are extracted and preserved for further processing
    • Traversed more deeper to build more informative structure for supporting nested params
    • Used appropriate regular expression as required at appropriate places
    • Efficient handling of:
      • Array Types
      • Union Types
      • Tuple Types
      • Function Parameters
      • Nested Object Parameters
      • Rest Parameters
      • Array of Tuples consisting of nested union and other params
  • Modified FES to parse and check the types at run time via generated Zod schema

    • Now, string checking via the presence of (?) as endsWith('?') is no longer required
    • Switch- Case is used to handle mappings from type names to Zod schemas

Additional Code Quality Work

  • Used fallback mechanism for safety and to avoid runtime errors
  • Explicit handling of types to ease overall maintainability and readability
  • Added in-line comments as appropriate

Trickiest Portions to handle

  • Nested Parameters like [p5.Color|String|Number|Number[], Number][]
  • Rest Parametrs "...Number[]"
  • Functions with parameters function(p5.Geometry)

Observations and Results

  • Tested and validated the parameterData.json to verify the expected results

Additional Checks

  • npm run docs - to generate the parameterData.json doc
  • npm run lint - lint checker for code style and standards
  • npm test - to re-validate all the test suites passed

@IIITM-Jay
Copy link
Contributor Author

IIITM-Jay commented May 24, 2025

Hi @davepagurek as part of the extensive work done for #7752 , here I am opening a PR to generate more informative and structural json format for parameterData.json. I really enjoyed doing it. Also, attached paarmeterData.json to verify the results as expected. More detailing I have given in the PR and also the file contains in-line comments as appropriately required. Requesting you to have a look and review on this PR.

The most trickiest part I encountered to handle was - [p5.Color|String|Number|Number[], Number][] where I need to handle Array of Tuples where Tuple contains unions where union again consists of varied types as arrays, strings etc. etc.

@ksen0 , @limzykenneth as it is a heavy change I would like to request your feedback as well. Just like to inform that this PR has been made by detailed discussions through exchange of thoughts between me and @davepagurek and consensus on the structure of the json to be build upon in the issue #7752 .

I hope you all would like my work.

And yes, Hoorrray! all the tests are also got passed, I felt that some of the flaky tests might get appeared again as sometimes it might appears.

Copy link
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

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

Thanks for your work on this, it's looking good!

So it looks like currently we're using typeObject and then have to re-parse structure out of the strings it returns. I'm wondering if maybe we can make a new function, e.g. structuredTypeObject, that does something similar to the original but preserves more of the original structure instead of condensing it into a string, while keeping around the usual typeObject function so that the output data.json (which is used on the p5 website) doesn't change?

return {
type: 'Function',
parameters,
...(returnType && { returnType }) // conditionally include returnType
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, I didn't know ... worked on undefined without errors, this is good to know!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Yeah, it's a handy little pattern — the spread operator with a short-circuit like ...(returnType && { returnType }) is a nice way to conditionally include properties without mutating the object. I actually picked this up while working with TypeScript, where patterns like this come up often in handling optional fields in typed object literals. Even though this is a JS file, bringing in some of those TS habits has definitely helped me keep things cleaner and more predictable.

// If the param is a function, populate parameters explicitly (only if not already done)
if (
typeInfo.type === 'Function' &&
!current[key].parameters &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know what circumstances would lead to having a function here but where typeObject(tag.type) doesn't return an object with a parameters key? I wonder if, rather than special-casing functions here, we can address that case in typeObject instead.

Copy link
Contributor Author

@IIITM-Jay IIITM-Jay May 31, 2025

Choose a reason for hiding this comment

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

Good catch — you're right to raise this. I have already handled this in typeobject with node type as FunctionType:

else if (node.type === 'FunctionType') {
  const parameters = node.params.map(p => typeObject(p));
  const returnType = node.result ? typeObject(node.result).type : undefined;
  return {
    type: 'Function',
    parameters,
    ...(returnType && { returnType })
  };
}

This check here in convert.mjs was meant as a safeguard in case some tag didn’t pass through typeObject properly or got overridden with incomplete type info — for example, if a parent param was documented vaguely or multiple @param(s) with overlapping paths were present. That said, if we trust that typeObject always runs and always returns the correct structure, then I agree this special-case logic may be redundant and we could enforce that structure more cleanly within typeObject itself.

Happy to refactor accordingly if we want to centralize the responsibility in typeObject.


} else {
// Intermediate node: create object structure for nested params
current[key] = current[key] || { type: 'Object', optional: true, properties: {} };
Copy link
Contributor

Choose a reason for hiding this comment

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

There's possibly a case here where we don't want this to be optional. Maybe if we say every time you have an object with properties, you have to document the object itself first? e.g. if you have:

/**
 * @param {Object} options
 * @param {number} options.sampleFactor
 */

...then as long as we add the less-deeply-nested @param first, we can use whatever type info we get from it. We could then possibly throw an error here if there's a deeply nested property documented before its parent (since we likely don't want that in the docs anyway.)

Copy link
Contributor Author

@IIITM-Jay IIITM-Jay May 31, 2025

Choose a reason for hiding this comment

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

Yes, Dave, I agree with your point. Initially, I added the optional: true as a safeguard in case the parent object wasn't explicitly documented. But on second thought, this scenario shouldn't occur at all — we should require that the parent object be documented before any of its nested properties. So, I agree that throwing an error here is the right approach

// Convert numeric-keyed object or array to array of processed params
if (Array.isArray(param.parameters)) {
result.parameters = param.parameters.map(processParam);
} else if (typeof param.parameters === 'object') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know under what circumstances we end up with an object here?

Copy link
Contributor Author

@IIITM-Jay IIITM-Jay May 31, 2025

Choose a reason for hiding this comment

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

yes, it somehow generates as follows, possibly from the overloads of loadModel, createModel . So to handle those I used the above as safeguards

{ '0': { type: 'Event' } }
{ '0': { type: 'p5.Geometry' } }

}

// Parse union type strings
const parseUnion = unionStr => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than parsing out arrays, tuples, etc here, could we modify typeObject to return something structured? Since we start with the structure, rather than condensing it and re-parsing it, we could just keep the original structure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of Course! why not we could definitely modify the typeObject without changing the output of data.json it's just that we need to handle all the corner and edge cases as how I handled explicitly for arrays, tuples, unions, etc.
The original structure just handles the top level parsing and not the deep nested ones so require handling of these explicitly to generate more structured output

@IIITM-Jay
Copy link
Contributor Author

IIITM-Jay commented May 31, 2025

Hi @davepagurek, thank you so, so , so... much for reviewing!

the output data.json (which is used on the p5 website) doesn't change?

Regarding your concern about changes to the data.json output used on the p5 website — I’ve tested the PR locally, and from what I observed, the output format hasn't changed. The only addition is a block related to generateZodSchemasForFunc, which appears because of the new JSDoc comment added for buildSchema() in param_validator.js.

/**
   * Recursively builds a Zod schema from a type descriptor object.
   *
   * @param {object} typeObj - An object describing the type.
   * @returns {ZodType} - A Zod schema representing the type.
   */

I believe this is unrelated to the core logic in convert.mjs, and doesn't affect existing functionality — just adds new schema generation support. It's possible the changes in param_validator.js might have caused some confusion here, so just flagging that in case it was missed during review 🙂

That said, I'm happy to revise or remove anything you think could lead to unintended effects — just let me know what you'd prefer!

@IIITM-Jay
Copy link
Contributor Author

Hi @davepagurek, thanks again for your thoughtful feedback throughout this PR.

From what I understand based on your comments above, you're suggesting that rather than handling special cases like functions externally (e.g. in convert.mjs), we should instead centralize all type structure logic within typeObject() itself — ensuring it always returns a fully structured and consistent output (e.g., including parameters for functions when relevant).

Just checking to confirm: is the goal here to simplify the usage of typeObject() by making it the single source of truth for all type-related processing, so downstream methods using it don’t need to do any further conditional handling?

If that's the direction you'd prefer, I’m happy to refactor accordingly. Please let me know if I’ve understood your vision correctly — I’ve taken a moment to carefully think through your points and want to ensure we’re aligned before moving forward with revisions.

@davepagurek
Copy link
Contributor

Just checking to confirm: is the goal here to simplify the usage of typeObject() by making it the single source of truth for all type-related processing, so downstream methods using it don’t need to do any further conditional handling?

I think so, since it would simplify the process of structured --> string --> parsed string --> structured to just structured --> different structured.

The one caveat is that this is just for producing parameterData.json for now -- I think it's OK to keep the format the same as it currently is for data.json and data.min.json, which is also generated from convert.mjs and is what gets displayed on the website. For FES, we need the deep structure because we need to check all the properties of objects, whereas on the website, we need a single string for each parameter to display. So I think that maybe means having two functions to produce types, one used when generating each file? One generating structured data, one just generating type strings as before?

@IIITM-Jay
Copy link
Contributor Author

I think it's OK to keep the format the same as it currently is for data.json and data.min.json, which is also generated from convert.mjs and is what gets displayed on the website.

Yes, I agree — it's important to keep the data.json and data.min.json formats unchanged, especially since they're used for the website and are derived from convert.mjs. That’s exactly why I’ve been careful to make changes in a way that doesn't affect those files.

So I think that maybe means having two functions ...

Regarding the idea of having two separate functions: I was thinking we could potentially keep things simpler by using a single function that conditionally outputs either the structured type (for FES) or the stringified type (for website display), depending on the use case. I’ll explore both approaches — separate vs conditional within one — and choose the one that offers the best balance of clarity, maintainability, and long-term flexibility, as to keep as simpler as possible for the longer run sustainability.

@davepagurek
Copy link
Contributor

Sounds good, thanks @IIITM-Jay!

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

Successfully merging this pull request may close these issues.

2 participants