uniforms icon indicating copy to clipboard operation
uniforms copied to clipboard

Using AutoField with schema containing patternProperties

Open harleyharl opened this issue 8 months ago • 1 comments

I've encountered an issue while attempting to use AutoField with a schema that contains patternProperties. Here's the relevant part of my schema:

"discrete_components": {
  "type": "object",
  "patternProperties": {
    ".+": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "maxLength": 255,
          "minLength": 1
        },
        "components": {
          "type": "array",
          "maxItems": 512,
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["type", "range_start", "range_end"],
            "properties": {
              "type": {
                "type": "string",
                "enum": ["OPEN", "CLOSED"]
              },
              "range_start": {
                "type": "integer",
                "maximum": 255
              },
              "range_end": {
                "type": "integer",
                "maximum": 255
              }
            }
          }
        }
      }
    }
  }
}

I'm trying to use AutoField like this:

<AutoField name="discrete_components.someId" />

However, this doesn't work because AutoField uses the getField function from the JSONSchemaBridge class, and that function doesn't handle patternProperties. Would it be possible to consider adding support for patternProperties to the JSONSchemaBridge class?

I've attempted to extend the class to address this issue, but it's a bit messy. Here's an example of what I've tried:

class CustomJSONSchemaBridge extends JSONSchemaBridge {

  fieldInvariant = (name: string, condition: boolean) => {
    invariant(condition, "Field not found in schema: \"%s\"", name)
  }

  partialNames = ["allOf", "anyOf", "oneOf"]

  resolveRefIfNeeded (
    partial: UnknownObject,
    schema: UnknownObject,
  ): UnknownObject {
    if (!("$ref" in partial)) {
      return partial
    }
  
    const { $ref, ...partialWithoutRef } = partial
    return this.resolveRefIfNeeded(
      // @ts-expect-error The `partial` and `schema` should be typed more precisely.
      Object.assign({}, partialWithoutRef, resolveRef($ref, schema)),
      schema,
    )
  }

  getField (name: string) {
    return joinName(null, name).reduce((definition, next, index, array) => {
      const prevName = joinName(array.slice(0, index))
      const nextName = joinName(prevName, next)
      const definitionCache = (this._compiledSchema[nextName] ??= {})
      definitionCache.isRequired = !!(
        definition.required?.includes(next) ||
        this._compiledSchema[prevName].required?.includes(next)
      )
  
      if (next === "$" || next === "" + parseInt(next, 10)) {
        this.fieldInvariant(name, definition.type === "array")
        definition = Array.isArray(definition.items)
          ? definition.items[parseInt(next, 10)]
          : definition.items
        this.fieldInvariant(name, !!definition)
      } else if (definition.type === "object" && definition.patternProperties) {
        // NOTE: This block has been added to handle patternProperties which aren't handled already. 
        let nextFound = false
        for (const pattern of Object.keys(definition.patternProperties)) {
          const regex = new RegExp(pattern)
          if (regex.test(next)) {
            definition = definition.patternProperties[pattern]
            nextFound = true
            break
          }
        }
        this.fieldInvariant(name, !!definition.properties)
      } else if (definition.type === "object") {
        this.fieldInvariant(name, !!definition.properties)
        definition = definition.properties[joinName.unescape(next)]
        this.fieldInvariant(name, !!definition)
      } else {
        let nextFound = false
        this.partialNames.forEach(partialName => {
          definition[partialName]?.forEach((partialElement: any) => {
            if (!nextFound) {
              partialElement = this.resolveRefIfNeeded(partialElement, this.schema)
              if (next in partialElement.properties) {
                definition = partialElement.properties[next]
                nextFound = true
              }
            }
          })
        })
  
        this.fieldInvariant(name, nextFound)
      }
  
      definition = this.resolveRefIfNeeded(definition, this.schema)
  
      // Naive computation of combined type, properties, and required.
      const required = definition.required ? definition.required.slice() : []
      const properties = definition.properties
        ? Object.assign({}, definition.properties)
        : {}
  
      this.partialNames.forEach(partialName => {
        definition[partialName]?.forEach((partial: any) => {
          partial = this.resolveRefIfNeeded(partial, this.schema)
  
          if (partial.required) {
            required.push(...partial.required)
          }
  
          Object.assign(properties, partial.properties)
  
          if (!definitionCache.type && partial.type) {
            definitionCache.type = partial.type
          }
        })
      })
  
      if (required.length > 0) {
        definitionCache.required = required
      }
  
      if (!isEmpty(properties)) {
        definitionCache.properties = properties
      }
  
      return definition
    }, this.schema)
  }
}

Is there a better approach that I may have overlooked? Your assistance is greatly appreciated! Thank you.

harleyharl avatar Oct 24 '23 23:10 harleyharl

Hi @harleyharl,

Would it be possible to consider adding support for patternProperties to the JSONSchemaBridge class?

We had a small internal discussion about it, and we came to the conclusion that we should always try to handle as many features as JSONSchema provides. Given that, we appreciate your effort and would happily accept a working solution as a PR. However, it still needs to pass the tests and possibly add new ones that cover the new functionality. If you don't have time to do it, then eventually, we'll provide this ourselves somewhere in the future.

TL;DR: Yes, it is possible and welcome to submit a PR with the support for patternProperties.

PS: Thanks for sharing your implementation.

kestarumper avatar Nov 06 '23 07:11 kestarumper