skip to Main Content

I am trying to generate pre compile standalone code like so:

https://ajv.js.org/standalone.html

I have an AJV instance created like so:

  const ajv = require("ajv");
  const add_formats = require("ajv-formats");
  const add_errors = require("ajv-errors");
  const ajv_inst = new ajv({
    code: { source: true }, // needed for standalone pre compiled scripts
    allErrors: true,
    $data: true,
  });
  add_errors(add_formats(ajv_inst));

However, I have custom keywords defined like so:

  ajv_inst.addKeyword({
    keyword: "custom_array",
    validate: (schema, data) => {
      try {
        const unique_name = data.map((tar) => {
          return tar.name.toLowerCase().replaceAll(/s+/g, "");
        });
        const unique_id = data.map((tar) => {
          return tar._id;
        });
        return (
          new Set(unique_name).size === data.length &&
          new Set(unique_id).size === data.length
        );
      } catch (err) {
        return false;
      }
    },
    metaSchema: {
      enum: [true],
    },
  });

In order to generate the standalone code with custom keywords, I think the keyword needs to be defined with a code generator function instead of the validate like so:

  code(cxt: KeywordCxt) {
    const {data, schema} = cxt
    const op = schema ? _`!==` : _`===`
    cxt.fail(_`${data} %2 ${op} 0`) // ... the only code change needed is to use `cxt.fail$data` here
  },

https://ajv.js.org/keywords.html#define-keyword-with-validate-function

questions:

  1. Given that I already have the validate property defined, is there a way to generate the pre compiled standalone code with the function defined as is in the validate property?
  2. If not, is there an easy or automated way to convert my validate function into the required code-gen code?

2

Answers


  1. Unfortunately, the validate function alone cannot be directly used for generating precompiled standalone code with AJV. This is because AJV requires custom keywords to define their behavior in terms of JavaScript code generation when creating standalone precompiled validators. The validate function is executed at runtime, while the code property is used during the compilation process to generate code.

    1. Is there a way to use the validate property for precompiled standalone code?

    No, AJV’s standalone generation mechanism requires the code property to define how the keyword translates to JavaScript code. The validate function cannot be directly used for this purpose.

    2. How to convert the validate function into code for standalone generation?

    To translate your validate function into a code generator function, you can:

    • Break down the logic of your validate function into reusable components.
    • Use the KeywordCxt object provided by AJV’s code property to inject equivalent logic during code generation.

    Here’s an example of how you might achieve this:

    Your Current Validate Function

    validate: (schema, data) => {
      try {
        const unique_name = data.map((tar) => {
          return tar.name.toLowerCase().replaceAll(/s+/g, "");
        });
        const unique_id = data.map((tar) => {
          return tar._id;
        });
        return (
          new Set(unique_name).size === data.length &&
          new Set(unique_id).size === data.length
        );
      } catch (err) {
        return false;
      }
    },
    

    Converted Code Generator Function

    Here’s a translation of this into the code generator:

    const {_, str} = require("ajv/dist/compile/codegen");
    
    ajv_inst.addKeyword({
      keyword: "custom_array",
      type: "array",
      schemaType: "boolean", // Expecting a boolean schema
      code(cxt) {
        const {data} = cxt; // The JSON data being validated
    
        // Generate a function to create unique name and ID arrays
        const uniqueNameCode = _`(${data}.map(tar => tar.name.toLowerCase().replace(/\s+/g, "")))`;
        const uniqueIdCode = _`(${data}.map(tar => tar._id))`;
    
        // Validate that the sets have the same length as the original array
        cxt.fail(
          _`new Set(${uniqueNameCode}).size !== ${data}.length || new Set(${uniqueIdCode}).size !== ${data}.length`
        );
      },
      metaSchema: { enum: [true] }, // The schema for the keyword itself
    });
    

    Explanation

    • KeywordCxt: The cxt object provides methods and properties to manipulate validation logic dynamically during code generation.
    • _ Syntax: The codegen utility _ is used to create JavaScript code expressions.
    • Inject Logic: We use cxt.fail to indicate a validation failure when the unique name or unique ID conditions are not met.

    Benefits of Using code for Standalone

    • The generated validator will include your custom keyword logic directly in the compiled output.

    • This approach avoids runtime dependencies and ensures efficient execution.

    Automating Conversion

    While there isn’t a direct tool to automate the conversion from a validate function to a code function, you can:

    • Use static analysis to identify your function’s dependencies and logic.
    • Write a helper function that dynamically maps runtime logic to compile-time expressions.

    For simple keywords, the conversion is often straightforward. However, for complex logic, it may require manual adjustments as seen above.

    Login or Signup to reply.
  2. For your first question: No, you cannot directly use the validate property for standalone code generation. The validate function runs at runtime which conflicts with the purpose of standalone code that needs to be generated at compile time.

    For converting your custom_array validate function to code generation, here’s how we can do it:

    import { _, KeywordCxt } from "ajv"
    
    ajv_inst.addKeyword({
      keyword: "custom_array",
      type: "array", // specify that this keyword works with arrays
      schemaType: "boolean", // schema value type
      code(cxt: KeywordCxt) {
        const { data, schema } = cxt;
        
        // Generate code that will run the same logic as your validate function
        const uniqueNameVar = cxt.gen.let('uniqueNames');
        const uniqueIdVar = cxt.gen.let('uniqueIds');
        
        cxt.gen.if(_`Array.isArray(${data})`, () => {
          // Generate map operation for names
          cxt.gen.assign(uniqueNameVar, 
            _`${data}.map(item => item.name.toLowerCase().replace(/s+/g, ""))`
          );
          
          // Generate map operation for ids
          cxt.gen.assign(uniqueIdVar,
            _`${data}.map(item => item._id)`
          );
          
          // Check uniqueness using Set
          cxt.fail(_`
            new Set(${uniqueNameVar}).size !== ${data}.length || 
            new Set(${uniqueIdVar}).size !== ${data}.length
          `);
        });
      },
      metaSchema: {
        enum: [true]
      }
    });
    

    Here’s a more complete example showing how to use this with standalone code generation:

    const Ajv = require("ajv");
    const standaloneCode = require("ajv/dist/standalone").default;
    
    // Create Ajv instance with standalone code generation enabled
    const ajv = new Ajv({
      code: { source: true },
      allErrors: true
    });
    
    // Add the custom keyword
    ajv.addKeyword({
      keyword: "custom_array",
      type: "array",
      code(cxt) {
        // ... code from above ...
      }
    });
    
    // Your schema
    const schema = {
      type: "object",
      properties: {
        items: {
          type: "array",
          custom_array: true,
          items: {
            type: "object",
            required: ["name", "_id"],
            properties: {
              name: { type: "string" },
              _id: { type: "string" }
            }
          }
        }
      }
    };
    
    // Compile the schema
    const validate = ajv.compile(schema);
    
    // Generate standalone code
    const moduleCode = standaloneCode(ajv, validate);
    
    // Save to file
    const fs = require("fs");
    fs.writeFileSync("validate.js", moduleCode);
    

    Key points to remember:

    1. The code function needs to generate JavaScript code rather than execute validation logic directly.

    2. Use the cxt.gen methods to generate variables and control flow.

    3. Use the tagged template literal helper _ from Ajv to safely generate code expressions.

    4. Error handling can be done using cxt.fail() which will generate appropriate error reporting code.

    5. Type information (type: "array") helps Ajv optimize the generated code.

    The main challenge in converting from validate to code is thinking in terms of code generation rather than runtime execution. You need to generate code that will perform the validation when executed, rather than performing the validation directly.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search