OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling / ollamatools-new / keet / node_modules / hyperschema / builder.cjs

498 lines · javascript ref:
const fs = require('fs')
const p = require('path')

const {
  SupportedTypes,
  getDefaultValue
} = require('./lib/types.js')
const generateCode = require('./lib/codegen')

const JSON_FILE_NAME = 'schema.json'
const CODE_FILE_NAME = 'index.js'

class ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    this.hyperschema = hyperschema
    this.description = description
    this.name = description?.name || fqn
    this.namespace = description?.namespace || ''
    this.derived = description.derived
    this.existing = existing
    this.fqn = fqn

    this.isPrimitive = false
    this.isEnum = false
    this.isStruct = false
    this.isArray = false
    this.isAlias = false
    this.isExternal = false

    this.version = -1
  }

  link () {}

  frameable () {
    return false
  }

  toJSON () {
    return {
      name: this.name,
      namespace: this.namespace
    }
  }
}

class Primitive extends ResolvedType {
  constructor (name) {
    super(null, name, name, null)
    this.isPrimitive = true
    this.bool = this.name === 'bool'
    this.default = getDefaultValue(this.name)
  }

  static AllPrimitives = new Map([...SupportedTypes].map(name => {
    return [name, new this(name)]
  }))
}

class Alias extends ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    super(hyperschema, fqn, description, existing)
    this.isAlias = true

    this.type = hyperschema.resolve(description.alias)
    if (!this.type) throw new Error(`Cannot resolve alias target ${description.alias} in ${description.name}`)

    this.default = this.type.default

    if (existing) {
      if (existing.type.name !== this.type.name) {
        throw new Error(`Remapping an alias: ${fqn}`)
      }
      this.version = existing.version
    } else if (!this.derived) {
      this.hyperschema.maybeBumpVersion()
      this.version = this.hyperschema.version
    }
  }

  frameable () {
    return this.type.frameable()
  }

  toJSON () {
    return {
      name: this.name,
      namespace: this.namespace,
      alias: this.type.fqn,
      version: this.version
    }
  }
}

class ExternalType extends ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    super(hyperschema, fqn, description, existing)

    this.isExternal = true
    this.filename = hyperschema.namespaces.get(description.namespace)?.external || null
    this.external = description.external
  }

  require (filename) {
    return p.relative(p.join(filename, '..'), p.resolve(this.filename))
      .replaceAll('\\', '/')
  }

  toJSON () {
    return {
      name: this.description.name,
      namespace: this.namespace,
      external: this.description.external
    }
  }
}

class Enum extends ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    super(hyperschema, fqn, description, existing)

    this.isEnum = true
    this.enum = []
    this.offset = typeof description.offset === 'number' ? description.offset : 1
    this.default = description.strings ? null : 0
    this.strings = !!description.strings

    if (this.existing) {
      if (!this.existing.enum) {
        throw new Error('Previous declaration was not an enum')
      }

      if (this.existing.enum.length > description.enum.length) {
        throw new Error('An enum value was removed')
      }
    }

    for (let i = 0; i < description.enum.length; i++) {
      const d = description.enum[i]
      const key = typeof d === 'string' ? d : d.key
      const prev = i < this.existing?.enum.length ? this.existing.enum[i] : null

      if (prev && prev.key !== key) {
        throw new Error(`Enum ${i} in ${fqn} changed. Was "${prev.key}" but is now "${key}`)
      }

      if (!prev) {
        hyperschema.maybeBumpVersion()
      }

      this.enum.push({
        key,
        version: prev ? prev.version : hyperschema.version
      })
    }
  }

  toJSON () {
    return {
      name: this.description.name,
      namespace: this.namespace,
      offset: this.offset,
      enum: this.enum
    }
  }
}

class StructField {
  constructor (hyperschema, struct, position, flag, description) {
    this.hyperschema = hyperschema
    this.description = description
    this.name = this.description.name
    this.required = this.description.required
    this.external = this.description.external

    this.position = position
    this.struct = struct
    this.flag = flag

    this.type = hyperschema.resolve(description.type) || null
    this.typeFqn = this.type ? this.type.fqn : description.type

    this.array = !!this.description.array

    this.version = description.version || hyperschema.version

    if (this.struct.existing) {
      const tag = `${this.struct.fqn}/${this.description.name}`
      const prevField = this.struct.existing.fields[position]

      if (prevField) {
        if (prevField.typeFqn !== this.typeFqn) {
          throw new Error(`Field was modified: ${tag}`)
        } else if (prevField.required !== this.required) {
          throw new Error(`A required field must always stay required: ${tag}`)
        }
        this.version = prevField.version
      } else if (!this.struct.derived) {
        hyperschema.maybeBumpVersion()
        this.version = hyperschema.version
      }
    }
  }

  link () {
    if (this.type === null) this.type = this.hyperschema.resolve(this.description.type) || null
    if (this.type === null) throw new Error(`Cannot resolve field type ${this.description.type} in ${this.name}`)
  }

  get framed () {
    return this.type.frameable()
  }

  toJSON () {
    return {
      name: this.description.name,
      required: this.description.required,
      array: this.description.array,
      type: this.typeFqn,
      version: this.version
    }
  }
}

class Array extends ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    super(hyperschema, fqn, description, existing)
    this.isArray = true
    this.default = null

    if (!description.type) {
      throw new Error(`Array ${this.fqn}: required 'type' definition is missing`)
    }

    this.type = hyperschema.resolve(description.type)
    this.framed = this.type.frameable()

    if (!description.name) {
      throw new Error(`Array ${this.fqn}: required 'name' definition is missing`)
    }

    if (!description.namespace) {
      throw new Error(`Array ${this.fqn}: required 'namespace' definition is missing`)
    }

    if (this.existing) {
      if (this.existing.type.fqn !== this.type.fqn) {
        throw new Error(`Array was modified: ${this.fqn}`)
      }
    }
  }

  toJSON () {
    return {
      name: this.name,
      namespace: this.namespace,
      array: true,
      type: this.type.fqn
    }
  }
}

class Struct extends ResolvedType {
  constructor (hyperschema, fqn, description, existing) {
    super(hyperschema, fqn, description, existing)
    this.isStruct = true
    this.default = null

    this.fields = []
    this.fieldsByName = new Map()

    this.optionals = []
    this.flagsPosition = -1
    this.linked = 0

    this.compact = !!description.compact

    if (Number.isInteger(description.flagsPosition)) {
      this.flagsPosition = description.flagsPosition
    }

    if (!description.name) {
      throw new Error(`Struct ${this.fqn}: required 'name' definition is missing`)
    }

    if (!description.fields) {
      throw new Error(`Struct ${this.fqn}: required 'fields' definition is missing`)
    }

    if (this.existing) {
      const oldLength = this.existing.fields.length
      const newLength = this.description.fields.length
      if (oldLength > newLength) {
        throw new Error(`A field was removed: ${this.fqn}`)
      } else if (this.compact && (oldLength !== newLength)) {
        throw new Error(`A compact struct was expanded: ${this.fqn}`)
      }
    } else if (!this.derived) {
      this.hyperschema.maybeBumpVersion()
      this.version = this.hyperschema.version
    }

    for (let i = 0; i < description.fields.length; i++) {
      const fieldDescription = description.fields[i]

      // bools can only be set in the flag, so auto downgrade the from required
      // TODO: if we add semantic meaning to required, ie "user MUST set this", we should
      // add an additional state for this
      if (fieldDescription.required && fieldDescription.type === 'bool') {
        fieldDescription.required = false
      }
      const flag = !fieldDescription.required ? 2 ** this.optionals.length : 0
      const field = new StructField(hyperschema, this, i, flag, fieldDescription)

      this.fields.push(field)
      this.fieldsByName.set(field.name, field)

      if (!fieldDescription.required) {
        this.optionals.push(field)
        if (this.flagsPosition === -1) {
          this.flagsPosition = i
        }
      }
    }
  }

  link () {
    for (const f of this.fields) f.link()
  }

  frameable () {
    return !this.compact
  }

  toJSON () {
    return {
      name: this.name,
      namespace: this.namespace,
      compact: this.compact,
      flagsPosition: this.flagsPosition,
      fields: this.fields.map(f => f.toJSON())
    }
  }
}

class HyperschemaNamespace {
  constructor (hyperschema, name) {
    this.hyperschema = hyperschema
    this.name = name
    this.external = null
  }

  require (filename) {
    this.external = filename
  }

  register (description) {
    return this.hyperschema.register({
      ...description,
      namespace: this.name
    })
  }
}

module.exports = class Hyperschema {
  constructor (json, { dir = null, versioned = true } = {}) {
    this.version = json ? json.version : versioned ? 0 : 1
    this.versioned = versioned
    this.schema = []
    this.dir = dir

    this.namespaces = new Map()
    this.positionsByType = new Map()
    this.typesByPosition = new Map()
    this.types = new Map()

    this.changed = false
    this.initializing = true
    if (json) {
      for (let i = 0; i < json.schema.length; i++) {
        const description = json.schema[i]
        this.register(description)
      }
    }
    this.initializing = false
    this.linkAll() // link all existing ones
  }

  static esm = false

  _getFullyQualifiedName (description) {
    if (description.namespace === null) return description.name
    return '@' + description.namespace + '/' + description.name
  }

  maybeBumpVersion () {
    if (this.changed || this.initializing) return
    this.changed = true
    if (this.versioned) this.version += 1
  }

  linkAll () {
    for (const t of this.types.values()) t.link()
  }

  register (description) {
    const fqn = this._getFullyQualifiedName(description)
    const existing = this.types.get(fqn)
    const existingPosition = this.positionsByType.get(fqn)

    let type = null
    if (description.alias) {
      type = new Alias(this, fqn, description, existing)
    } else if (description.enum) {
      type = new Enum(this, fqn, description, existing)
    } else if (description.array) {
      type = new Array(this, fqn, description, existing)
    } else if (description.external) {
      type = new ExternalType(this, fqn, description, existing)
    } else {
      type = new Struct(this, fqn, description, existing)
    }
    this.types.set(fqn, type)

    const json = type.toJSON()
    if (existing) {
      this.schema[existingPosition] = json
    } else {
      const position = this.schema.push(json) - 1
      this.positionsByType.set(fqn, position)
      this.typesByPosition.set(position, fqn)
    }

    return type
  }

  namespace (name) {
    if (this.namespaces.has(name)) throw new Error('Namespace already exists')
    const ns = new HyperschemaNamespace(this, name)
    this.namespaces.set(name, ns)
    return ns
  }

  resolve (fqn, { aliases = true } = {}) {
    if (Primitive.AllPrimitives.has(fqn)) return Primitive.AllPrimitives.get(fqn)
    const type = this.types.get(fqn)
    if (!aliases && type.isAlias) return type.type
    return type
  }

  toJSON () {
    this.linkAll()

    const json = { version: this.version, schema: this.schema.filter(t => !t.derived) }
    return json
  }

  toCode ({ esm = this.constructor.esm, filename } = {}) {
    this.linkAll()

    return generateCode(this, { esm, filename })
  }

  static toDisk (hyperschema, dir, opts) {
    if (typeof dir === 'object' && dir) {
      opts = dir
      dir = null
    }

    hyperschema.linkAll()

    if (!dir) dir = hyperschema.dir
    fs.mkdirSync(dir, { recursive: true })

    const jsonPath = p.join(p.resolve(dir), JSON_FILE_NAME)
    const codePath = p.join(p.resolve(dir), CODE_FILE_NAME)

    fs.writeFileSync(jsonPath, JSON.stringify(hyperschema.toJSON(), null, 2), { encoding: 'utf-8' })
    fs.writeFileSync(codePath, hyperschema.toCode({ ...opts, filename: codePath }), { encoding: 'utf-8' })
  }

  static from (json, opts) {
    if (typeof json === 'string') {
      const jsonFilePath = p.join(p.resolve(json), JSON_FILE_NAME)
      let exists = false
      try {
        fs.statSync(jsonFilePath)
        exists = true
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
      if (exists) return new this(JSON.parse(fs.readFileSync(jsonFilePath)), { ...opts, dir: json })
      return new this(null, { ...opts, dir: json })
    }
    return new this(json, opts)
  }
}