Skip to content
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

Enums #14

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,56 @@ class Alias extends ResolvedType {
}
}

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
Expand Down Expand Up @@ -256,6 +306,8 @@ module.exports = class Hyperschema {
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 {
type = new Struct(this, fqn, description, existing)
}
Expand Down
119 changes: 107 additions & 12 deletions lib/codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ module.exports = function generateSchema (hyperschema) {
if (type.isStruct) {
structs.push(type)
}
if (type.isEnum) {
structs.push(type)
}
}

for (let i = 0; i < structs.length; i++) {
const struct = structs[i]
const id = 'encoding' + i
const encoder = generateStruct(id, struct)
const encoder = struct.enum ? generateEnum(id, struct) : generateStruct(id, struct)
structsByName.set(struct.fqn, { encoder, id })
}

Expand Down Expand Up @@ -57,6 +60,18 @@ module.exports = function generateSchema (hyperschema) {
str += '}\n'
str += '\n'

str += 'function getEnum (name) {\n'
str += ' switch (name) {\n'
for (let i = 0; i < structs.length; i++) {
const struct = structs[i]
if (struct.enum) str += ` case ${s(struct.fqn)}: return ${structsByName.get(struct.fqn).id}_enum\n`
}
str += ' default: throw new Error(\'Enum not found \' + name)\n'
str += ' }\n'
str += '}\n'

str += '\n'

str += 'function getEncoding (name) {\n'
str += ' switch (name) {\n'
for (let i = 0; i < structs.length; i++) {
Expand All @@ -69,7 +84,7 @@ module.exports = function generateSchema (hyperschema) {

str += '\n'

str += 'function resolveStruct (name, v = VERSION) {\n'
str += 'function getStruct (name, v = VERSION) {\n'
str += ' const enc = getEncoding(name)\n'
str += ' return {\n'
str += ' preencode (state, m) {\n'
Expand All @@ -87,10 +102,81 @@ module.exports = function generateSchema (hyperschema) {
str += ' }\n'
str += '}\n'
str += '\n'
str += 'module.exports = { resolveStruct, getEncoding, encode, decode, setVersion, version }\n'
str += 'module.exports = { resolveStruct: getStruct, getStruct, getEnum, getEncoding, encode, decode, setVersion, version }\n'

return str

function generateEnum (id, struct) {
let str = ''

str += `const ${id}_enum = {\n`

for (let i = 0; i < struct.enum.length; i++) {
const e = struct.enum[i]
const value = struct.strings ? s(e.key) : i + struct.offset
str += ` ${gen.property(e.key)}: ${value}${i < struct.enum.length - 1 ? ',' : ''}\n`
}

str += '}\n\n'

const preencode = generateEncode({ preencode: true })
const encode = generateEncode()
const decode = generateDecode()

str += `// ${struct.fqn} enum\n`
str += `const ${id} = {\n`
str += ' preencode (state, m) {\n'
str += `${preencode}`
str += ' },\n'
str += ' encode (state, m) {\n'
str += `${encode}`
str += ' },\n'
str += ' decode (state) {\n'
str += `${decode}`
str += ' }\n'
str += '}\n'

return str

function generateEncode ({ preencode = false } = {}) {
const max = struct.enum.length + struct.offset - 1
const encode = preencode ? 'preencode' : 'encode'

if (preencode && max <= 0xfc) {
return ` state.end++ // max enum is ${max} so always one byte\n`
}

if (!struct.strings) {
let str = ''
str += ` if (m > ${max}) throw new Error('Unknown enum')\n`
str += ` c.uint.${encode}(state, m)\n`
return str
}

let str = ' switch (m) {\n'
for (let i = 0; i < struct.enum.length; i++) {
str += ` case ${s(struct.enum[i].key)}:\n`
str += ` c.uint.${encode}(state, ${i + struct.offset})\n`
str += ' break\n'
}
str += ' default: throw new Error(\'Unknown enum\')\n'
str += ' }\n'
return str
}

function generateDecode () {
if (!struct.strings) return ' return c.uint.decode(state)\n'

let str = ' switch (c.uint.decode(state)) {\n'
for (let i = 0; i < struct.enum.length; i++) {
str += ` case ${i + struct.offset}: return ${s(struct.enum[i].key)}\n`
}
str += ' default: return null\n'
str += ' }\n'
return str
}
}

function generateStruct (id, struct) {
let str = ''
const fieldTypes = new Map()
Expand Down Expand Up @@ -119,9 +205,9 @@ module.exports = function generateSchema (hyperschema) {
str += '\n'
}

const preencode = generateEncode(struct, { preencode: true })
const encode = generateEncode(struct)
const decode = generateDecode(struct)
const preencode = generateEncode({ preencode: true })
const encode = generateEncode()
const decode = generateDecode()
str += `// ${struct.fqn}\n`
str += `const ${id} = ${struct.array ? 'c.array(' + (struct.compact ? '' : 'c.frame(') : ''}{\n`
str += ' preencode (state, m) {\n'
Expand All @@ -136,22 +222,27 @@ module.exports = function generateSchema (hyperschema) {
str += `}${struct.array ? (struct.compact ? '' : ')') + ')' : ''}\n`
return str

function generateEncode (struct, { preencode = false } = {}) {
function generateEncode ({ preencode = false } = {}) {
const fn = preencode ? 'preencode' : 'encode'
let str = ''
let maxFlag = 0
let fastFlags = false

const flags = []
for (let i = 0; i < struct.optionals.length; i++) {
const field = struct.optionals[i]
maxFlag = Math.max(maxFlag, field.flag)
if (field.external) continue
flags.push('(' + vInlinePrefix(field.version, gen('m', field.name)) + ' ? ' + field.flag + ' : 0)')
}

if (flags.length) {
if (flags.length === 1) {
str += ' const flags = ' + flags[0].slice(1, -1) + '\n\n'
if (maxFlag < 128 && preencode) {
fastFlags = true
} else if (flags.length === 1) {
str += ` const flags = ${flags[0].slice(1, -1)}\n\n`
} else {
str += ' const flags =\n ' + flags.join(' |\n ') + '\n\n'
str += ` const flags =\n ${flags.join(' |\n ')}\n\n`
}
}

Expand All @@ -162,7 +253,11 @@ module.exports = function generateSchema (hyperschema) {
if (field.external) continue

if (i === struct.flagsPosition) {
str += ` c.uint.${fn}(state, flags)`
if (fastFlags) {
str += ` state.end++ // max flag is ${maxFlag} so always one byte`
} else {
str += ` c.uint.${fn}(state, flags)`
}
bitfield = true
}
if (field.type.bool) continue
Expand Down Expand Up @@ -198,7 +293,7 @@ module.exports = function generateSchema (hyperschema) {
return str
}

function generateDecode (struct) {
function generateDecode () {
let str = ''
let seen = 0

Expand Down
93 changes: 93 additions & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,96 @@ test('basic struct array', async t => {
t.alike(dec, [{ foo: 'bar' }, { foo: 'baz' }])
}
})

test('basic enums', async t => {
const schema = await createTestSchema(t)

await schema.rebuild(schema => {
const ns = schema.namespace('test')
ns.register({
name: 'test-enum',
enum: [
'hello',
'world'
]
})

ns.register({
name: 'test-struct',
fields: [
{
name: 'foo',
type: '@test/test-enum',
required: true
}
]
})
})

{
const { hello } = schema.module.getEnum('@test/test-enum')
const enc = schema.module.resolveStruct('@test/test-struct')
const buf = c.encode(enc, { foo: hello })
const dec = c.decode(enc, buf)

t.alike(dec, { foo: hello })
}

{
const { world } = schema.module.getEnum('@test/test-enum')
const enc = schema.module.resolveStruct('@test/test-struct')
const buf = c.encode(enc, { foo: world })
const dec = c.decode(enc, buf)

t.alike(dec, { foo: world })
}

t.alike(schema.module.getEnum('@test/test-enum'), { hello: 1, world: 2 })
})

test('basic enums (strings)', async t => {
const schema = await createTestSchema(t)

await schema.rebuild(schema => {
const ns = schema.namespace('test')
ns.register({
name: 'test-enum',
strings: true,
enum: [
'hello',
'world'
]
})

ns.register({
name: 'test-struct',
fields: [
{
name: 'foo',
type: '@test/test-enum',
required: true
}
]
})
})

{
const { hello } = schema.module.getEnum('@test/test-enum')
const enc = schema.module.resolveStruct('@test/test-struct')
const buf = c.encode(enc, { foo: hello })
const dec = c.decode(enc, buf)

t.alike(dec, { foo: hello })
}

{
const { world } = schema.module.getEnum('@test/test-enum')
const enc = schema.module.resolveStruct('@test/test-struct')
const buf = c.encode(enc, { foo: world })
const dec = c.decode(enc, buf)

t.alike(dec, { foo: world })
}

t.alike(schema.module.getEnum('@test/test-enum'), { hello: 'hello', world: 'world' })
})
Loading