/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict"
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("./assert")
const Accessor = require("./accessors").Accessor
const BinaryRecord = require("./binary-record").BinaryRecord
const registry = require("./text-encoder-registry")
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const VALID_RECORD_NAME = /^[A-Z][a-zA-Z0-9_$]*$/
const VALID_FIELD_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
const INVALID_FIELD_NAME = /^(?:__(?:(?:define|lookup)(?:G|S)etter|proto|noSuchMethod)__|constructor|hasOwnProperty|isPrototypeOf|propertyIsEnumerable|toJSON|toLocaleString|toString|valueOf)$/
/**
* Creates the registry of record classes for sub records.
*
* @param {object} pool - The registry of record classes.
* @param {object} field - The field definition to add to the registry.
* @returns {object} Pass through the `pool`.
*/
function makeRecordPool(pool, field) {
const accessor = field.accessor
if ("Record" in accessor && !(accessor.uid in pool)) {
pool[accessor.uid] = accessor.Record
}
return pool
}
/**
* Generates the initialization code of an object record class.
*
* @param {Array} fields - The definition of all fields.
* @returns {string} The initialization code.
* @private
*/
function initCode(fields) {
if (!fields.some(f => f.accessor.hasInitCode)) {
return ""
}
return `this[s.subRecords] = Object.create(null)
${
fields
.filter(f => f.accessor.hasInitCode)
.map(f => f.accessor.initCode(f.name, f.offset))
.join("\n ")
}
Object.freeze(this[s.subRecords])`
}
/**
* Generates the getter/setter code of an object record class.
*
* @param {Array} fields - The definition of all fields.
* @returns {string} The getter/setter code.
* @private
*/
function propertiesCode(fields) {
return fields
.map(f => f.accessor.propertyCode(f.name, f.offset))
.join("\n ")
}
/**
* Generates the `keys` method code of an object record class.
*
* @param {Array} fields - The definition of all fields.
* @returns {string} The `keys` method code.
* @private
*/
function keysCode(fields) {
return fields
.map(f => `yield ${JSON.stringify(f.name)}`)
.join("\n ")
}
/**
* Generates the `toJSON` method code of an object record class.
*
* @param {Array} fields - The definition of all fields.
* @returns {string} The `toJSON` method code.
* @private
*/
function toJSONCode(fields) {
return fields
.map(f => `${f.name}: this.${f.name},`)
.join("\n ")
}
/**
* Defines a new `ObjectRecord` class.
*
* @param {Array} definition - The definitions to define.
* @returns {class} Defined class.
* @private
*/
function defineObjectRecord(definition) {
const className = definition.className
const bitLength = definition.bitLength
const fields = definition.fields
const byteLength = (bitLength >> 3) + ((bitLength & 0x07) ? 1 : 0)
const TextEncoder = registry.getTextEncoder()
const RecordPool = fields.reduce(makeRecordPool, {})
return Function("assert", "BinaryRecord", "TextEncoder", "RecordPool", `
"use strict"
const s = BinaryRecord.symbols
class ${className} extends BinaryRecord {
constructor(buffer, byteOffset) {
super(buffer, byteOffset)
${initCode(fields)}
}
static view(buffer, byteOffset) {
return Object.freeze(new ${className}(buffer, byteOffset))
}
static get bitLength() {
return ${bitLength}
}
static get byteLength() {
return ${byteLength}
}
static* keys() {
${keysCode(fields)}
}
${propertiesCode(fields)}
toJSON() {
return {
${toJSONCode(fields)}
}
}
}
return ${className}
`)(assert, BinaryRecord, TextEncoder, RecordPool)
}
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
exports.getObjectRecord = function getObjectRecord(definition) {
assert.string(definition.className, "className")
assert(
VALID_RECORD_NAME.test(definition.className),
"'className' should be a PascalCase Identifier, " +
`but got ${JSON.stringify(definition.className)}.`
)
assert.integer(definition.bitLength, "bitLength")
assert.instanceOf(definition.fields, Array, "fields")
const names = new Set()
definition.fields.forEach((field, index) => {
assert.object(field, `fields[${index}]`)
assert.string(field.name, `fields[${index}].name`)
assert(
VALID_FIELD_NAME.test(field.name),
`'fields[${index}].name' should be a camelCase identifier, ` +
`but got ${JSON.stringify(field.name)}.`
)
assert(
!INVALID_FIELD_NAME.test(field.name),
`'fields[${index}].name' should be a valid name, ` +
`but got an forbidden name ${field.name}.`
)
assert(
!names.has(field.name),
`'fields[${index}].name' should not be duplicate of other field ` +
`names, but got duplicate name ${field.name}.`
)
assert.instanceOf(field.accessor, Accessor, `fields[${index}].accessor`)
assert.integer(field.offset, `fields[${index}].offset`)
assert.gte(field.offset, 0, `fields[${index}].offset`)
names.add(field.name)
})
return defineObjectRecord(definition)
}