0.2.5 initial commit

Makefile incomplete
This commit is contained in:
Aiden McClelland
2020-11-23 13:44:28 -07:00
commit 95d3845906
503 changed files with 53448 additions and 0 deletions

View File

@@ -0,0 +1,446 @@
import {
ValueSpec, ConfigSpec, UniqueBy, ValueSpecOf, ValueType
} from './config-types'
import * as pointer from 'json-pointer'
import * as handlebars from 'handlebars'
import { Annotations, getDefaultObject, getDefaultUnion, listInnerSpec, mapConfigSpec, Range } from './config-utilities'
export class ConfigCursor<T extends ValueType> {
private cachedSpec?: ValueSpecOf<T>
constructor (
private readonly rootSpec: ConfigSpec,
private readonly rootOldConfig: object,
private readonly rootMappedConfig: object = null,
private readonly rootConfig: object = null,
private readonly ptr: string = '',
) {
if (!this.rootOldConfig) {
this.rootOldConfig = getDefaultObject(this.rootSpec)
}
if (!this.rootMappedConfig) {
this.rootMappedConfig = JSON.parse(JSON.stringify(this.rootOldConfig))
mapConfigSpec(this.rootSpec, this.rootMappedConfig)
}
if (!this.rootConfig) {
this.rootConfig = JSON.parse(JSON.stringify(this.rootMappedConfig))
}
}
seek<S extends ValueType> (ptr: string): ConfigCursor<S> {
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
pointer.compile(
pointer.parse(this.ptr)
.concat(pointer.parse(ptr)),
),
)
}
seekNext<S extends ValueType> (key: string | number): ConfigCursor<S> {
return this.seek(pointer.compile([`${key}`]))
}
unseek<S extends ValueType> (levels?: number): ConfigCursor<S> {
let ptr: string
if (levels === undefined) {
ptr = ''
} else {
// TODO, delete or make use of, it isn't being used so far
// This is not being used so far
let ptr_arr = pointer.parse(this.ptr)
for (let i = 0; i < levels; i++) {
ptr_arr.pop()
}
ptr = pointer.compile(ptr_arr)
}
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
ptr,
)
}
key (): string {
return pointer.parse(this.ptr).pop()
}
oldConfig (): any {
if (pointer.has(this.rootOldConfig, this.ptr)) {
return pointer.get(this.rootOldConfig, this.ptr)
} else {
return undefined
}
}
mappedConfig (): any {
if (pointer.has(this.rootMappedConfig, this.ptr)) {
return pointer.get(this.rootMappedConfig, this.ptr)
} else {
return undefined
}
}
toString (): string {
const spec: ValueSpec = this.spec()
const config = this.config()
switch (spec.type) {
case 'string':
return config
case 'number':
return `${config}${spec.units ? ' ' + spec.units : ''}`
case 'object':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : ''
case 'union':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id]
case 'pointer':
return 'System Defined'
default:
return ''
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
config (): any {
if (pointer.has(this.rootConfig, this.ptr)) {
return pointer.get(this.rootConfig, this.ptr)
} else {
return undefined
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
spec (): ValueSpecOf<T> {
if (this.cachedSpec) return this.cachedSpec
const parsed = pointer.parse(this.ptr)
// We elevate the rootSpec (ConfigSpec) to a dummy ValueSpecObject
let ret: ValueSpec = {
type: 'object',
spec: this.rootSpec,
nullable: false,
nullByDefault: false,
name: 'Config',
displayAs: 'Config',
uniqueBy: null,
}
let ptr = []
for (let seg of parsed) {
switch (ret.type) {
case 'object':
ret = ret.spec[seg]
break
case 'union':
if (seg === ret.tag.id) {
ret = {
type: 'enum',
default: ret.default,
values: Object.keys(ret.variants),
name: ret.tag.name,
description: ret.tag.description,
valueNames: ret.tag.variantNames,
}
} else {
const cfg = this.unseek().seek(pointer.compile(ptr))
ret = ret.variants[cfg.config()[ret.tag.id]][seg]
}
break
case 'list':
//in essence, for a list we replace the list typed ValueSpecOf with it's internal ListValueSpec, a ValueSpecOf<T> where config @ ptr is of type T[].
// we also append default values to it.
// note also that jsonKey is not used. jsonKey in this case is an index of an array, like 0, 1, etc.
// this implies that every index of a list has an identical inner spec
ret = listInnerSpec(ret)
break
default:
return undefined
}
if (ret === undefined) break
ptr.push(seg)
}
this.cachedSpec = ret as ValueSpecOf<T>
return this.cachedSpec
}
checkInvalid (): string | null { // null if valid
const spec: ValueSpec = this.spec()
const cfg = this.config()
switch (spec.type) {
case 'string':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'string') {
if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) {
return null
} else {
return spec.patternDescription
}
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'number':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'number') {
if (spec.integral && cfg != Math.trunc(cfg)) {
return `${spec.name} must be an integer.`
}
try {
Range.from(spec.range).checkIncludes(cfg)
return null
} catch (e) {
return e.message
}
} else {
throw new TypeError(`${this.ptr}: expected number, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'boolean':
if (typeof cfg === 'boolean') {
return null
} else {
throw new TypeError(`${this.ptr}: expected boolean, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'enum':
if (typeof cfg === 'string') {
return spec.values.includes(cfg) ? null : `${cfg} is not a valid selection.`
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'list':
if (Array.isArray(cfg)) {
const range = Range.from(spec.range)
const min = range.integralMin()
const max = range.integralMax()
const length = cfg.length
if (min && length < min) {
return spec.subtype === 'enum' ? 'Not enough options selected.' : 'List is too short.'
}
if (max && length > max) {
return spec.subtype === 'enum' ? 'Too many options selected.' : 'List is too long.'
}
for (let idx in cfg) {
let cursor = this.seekNext(idx)
if (cursor.checkInvalid()) {
return `Item #${idx + 1} is invalid. ${cursor.checkInvalid()}`
}
for (let idx2 in cfg) {
if (idx !== idx2 && cursor.equals(this.seekNext(idx2))) {
return `Item #${idx + 1} is not unique.`
}
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected array, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'object':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
for (let idx in spec.spec) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.spec[idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'pointer':
return null
case 'union':
if (typeof cfg === 'object' && !Array.isArray(cfg)) {
if (typeof cfg[spec.tag.id] === 'string') {
for (let idx in spec.variants[cfg[spec.tag.id]]) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.variants[cfg[spec.tag.id]][idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}/${spec.tag.id}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
}
}
isNew (): boolean {
const oldCfg = this.oldConfig()
const mappedCfg = this.mappedConfig()
if (mappedCfg && oldCfg && typeof mappedCfg === 'object' && typeof oldCfg === 'object') {
for (let key in mappedCfg) {
if (this.seekNext(key).isNew()) return true
}
return false
} else {
return mappedCfg !== oldCfg
}
}
isEdited (): boolean {
const cfg = this.config()
const mappedCfg = this.mappedConfig()
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
const spec = this.spec()
let allKeys
if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'>
const labelForSelection = unionSpec.tag.id
allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
} else {
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
}
for (let key of allKeys) {
if (this.seekNext(key).isEdited()) return true
}
return false
} else {
return cfg !== mappedCfg
}
}
equals (cursor: ConfigCursor<T>): boolean {
const lhs = this.config()
const rhs = cursor.config()
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'string':
case 'number':
case 'boolean':
case 'enum':
return lhs === rhs
case 'object':
case 'union':
return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>)
case 'list':
if (lhs.length !== rhs.length) {
return false
}
for (let idx = 0; idx < lhs.length; idx++) {
if (!this.seekNext(`${idx}`).equals(cursor.seekNext(`${idx}`))) {
return false
}
}
return true
default:
return false
}
}
getAnnotations (): Annotations<T> {
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'object': {
const ret: Annotations<'object'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: { },
}
for (let key in spec.spec) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'union': {
const ret: Annotations<'union'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: {
[spec.tag.id]: this.seekNext<'enum'>(spec.tag.id).getAnnotations(),
},
}
for (let key in spec.variants[this.config()[spec.tag.id]]) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'list': {
const ret: Annotations<'list'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: [],
}
for (let key in this.config()) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
default:
return {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
} as Annotations<T>
}
}
async createFirstEntryForList () {
const spec: ValueSpec = this.spec()
if (spec.type === 'object' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultObject(spec.spec))
}
if (spec.type === 'union' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultUnion(spec))
}
}
injectModalData (res: { data?: any }): void {
if (res.data !== undefined) {
pointer.set(this.rootConfig, this.ptr, res.data)
}
}
}
function isEqual (uniqueBy: UniqueBy, lhs: ConfigCursor<'object'>, rhs: ConfigCursor<'object'>): boolean {
if (uniqueBy === null) {
return false
} else if (typeof uniqueBy === 'string') {
return lhs.seekNext(uniqueBy).equals(rhs.seekNext(uniqueBy))
} else if ('any' in uniqueBy) {
for (let subSpec of uniqueBy.any) {
if (isEqual(subSpec, lhs, rhs)) {
return true
}
}
return false
} else if ('all' in uniqueBy) {
for (let subSpec of uniqueBy.all) {
if (!isEqual(subSpec, lhs, rhs)) {
return false
}
}
return true
}
}

View File

@@ -0,0 +1,133 @@
export interface ConfigSpec { [key: string]: ValueSpec }
export type ValueType = 'string' | 'number' | 'boolean' | 'enum' | 'list' | 'object' | 'pointer' | 'union'
export type ValueSpec = ValueSpecOf<ValueType>
// core spec types. These types provide the metadata for performing validations
export type ValueSpecOf<T extends ValueType> =
T extends 'string' ? ValueSpecString :
T extends 'number' ? ValueSpecNumber :
T extends 'boolean' ? ValueSpecBoolean :
T extends 'enum' ? ValueSpecEnum :
T extends 'list' ? ValueSpecList :
T extends 'object' ? ValueSpecObject :
T extends 'pointer' ? ValueSpecPointer :
T extends 'union' ? ValueSpecUnion :
never
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string'
default?: DefaultString
nullable: boolean
masked: boolean
copyable: boolean
}
export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone {
type: 'number'
nullable: boolean
default?: number
}
export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone {
type: 'enum'
default: string
}
export interface ValueSpecBoolean extends WithStandalone {
type: 'boolean'
default: boolean
}
export interface ValueSpecUnion extends ListValueSpecUnion, WithStandalone {
type: 'union'
}
export interface ValueSpecPointer extends WithStandalone {
type: 'pointer'
subtype: 'app' | 'system'
target: 'lan-address' | 'tor-address' | 'config'
'app-id': string
}
export interface ValueSpecObject extends ListValueSpecObject, WithStandalone {
type: 'object'
nullable: boolean
nullByDefault: boolean
}
export interface WithStandalone {
name: string
description?: string
changeWarning?: string
}
// no lists of booleans, lists, pointers
export type ListValueSpecType = 'string' | 'number' | 'enum' | 'object' | 'union'
// represents a spec for the values of a list
export type ListValueSpecOf<T extends ListValueSpecType> =
T extends 'string' ? ListValueSpecString :
T extends 'number' ? ListValueSpecNumber :
T extends 'enum' ? ListValueSpecEnum :
T extends 'object' ? ListValueSpecObject :
T extends 'union' ? ListValueSpecUnion :
never
// represents a spec for a list
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStandalone {
type: 'list'
subtype: T
spec: ListValueSpecOf<T>
range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: string[] | number[] | DefaultString[] | object[]
}
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList, s: S): t is ValueSpecListOf<S> {
return t.subtype === s
}
export interface ListValueSpecString {
pattern?: string
patternDescription?: string
}
export interface ListValueSpecNumber {
range: string
integral: boolean
units?: string
}
export interface ListValueSpecEnum {
values: string[]
valueNames: { [value: string]: string }
}
export interface ListValueSpecObject {
spec: ConfigSpec //this is a mapped type of the config object at this level, replacing the object's values with specs on those values
uniqueBy: UniqueBy //indicates whether duplicates can be permitted in the list
displayAs?: string //this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
}
export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
export interface ListValueSpecUnion {
tag: UnionTagSpec
variants: { [key: string]: ConfigSpec }
displayAs?: string //this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id
uniqueBy: UniqueBy
default: string //this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list
}
export interface UnionTagSpec {
id: string //The name of the field containing one of the union variants
name: string
description?: string
variantNames: { //the name of each variant
[variant: string]: string
}
}
export type DefaultString = string | { charset: string, len: number }

View File

@@ -0,0 +1,413 @@
import {
ValueSpec, ValueSpecList, DefaultString, ValueSpecUnion, ConfigSpec,
ValueSpecObject, ValueSpecString, ValueSpecEnum, ValueSpecNumber,
ValueSpecBoolean, ValueSpecPointer, ValueSpecOf, ListValueSpecType
} from './config-types'
export interface Annotation {
invalid: string | null
edited: boolean
added: boolean
}
export type Annotations<T extends string> =
T extends 'object' | 'union' ? { self: Annotation, members: { [key: string]: Annotation } } :
T extends 'list' ? { self: Annotation, members: Annotation[] } :
Annotation
export class Range {
min?: number
max?: number
minInclusive: boolean
maxInclusive: boolean
static from (s: string): Range {
const r = new Range()
r.minInclusive = s.startsWith('[')
r.maxInclusive = s.endsWith(']')
const [minStr, maxStr] = s.split(',').map(a => a.trim())
r.min = minStr === '(*' ? undefined : Number(minStr.slice(1))
r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1))
return r
}
checkIncludes (n: number) {
if (this.hasMin() !== undefined && ((!this.minInclusive && this.min == n || (this.min > n)))) {
throw new Error(`Value must be ${this.minMessage()}.`)
}
if (this.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) {
throw new Error(`Value must be ${this.maxMessage()}.`)
}
}
hasMin (): boolean {
return this.min !== undefined
}
hasMax (): boolean {
return this.max !== undefined
}
minMessage (): string {
return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}`
}
maxMessage (): string {
return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}`
}
description (): string {
let message = 'Value can be any number.'
if (this.hasMin() || this.hasMax()) {
message = 'Value must be'
}
if (this.hasMin() && this.hasMax()) {
message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.`
} else if (this.hasMin() && !this.hasMax()) {
message = `${message} ${this.minMessage()}.`
} else if (!this.hasMin() && this.hasMax()) {
message = `${message} ${this.maxMessage()}.`
}
return message
}
integralMin (): number | undefined {
if (this.min) {
const ceil = Math.ceil(this.min)
if (this.minInclusive) {
return ceil
} else {
if (ceil === this.min) {
return ceil + 1
} else {
return ceil
}
}
}
}
integralMax (): number | undefined {
if (this.max) {
const floor = Math.floor(this.max)
if (this.maxInclusive) {
return floor
} else {
if (floor === this.max) {
return floor - 1
} else {
return floor
}
}
}
}
}
// converts a ValueSpecList, i.e. a spec for a list, to its inner ListValueSpec, i.e., a spec for the values within the list.
// We then augment it with the defalt values (e.g. nullable: false) to make a
export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf<ListValueSpecType> {
return {
type: listSpec.subtype,
nullable: false,
name: listSpec.name,
description: listSpec.description,
changeWarning: listSpec.changeWarning,
...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype
}
}
export function mapSpecToConfigValue (spec: ValueSpec, value: any): any {
if (value === undefined) return undefined
switch (spec.type) {
case 'string': return mapStringSpec(value)
case 'number': return mapNumberSpec(value)
case 'boolean': return mapBooleanSpec(spec, value)
case 'enum': return mapEnumSpec(spec, value)
case 'list': return mapListSpec(spec, value)
case 'object': return mapObjectSpec(spec, value)
case 'union': return mapUnionSpec(spec, value)
case 'pointer': return value
}
}
export function mapConfigSpec (configSpec: ConfigSpec, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.entries(configSpec).map(([key, val]) => {
value[key] = mapSpecToConfigValue(val, value[key])
if (value[key] === undefined) {
value[key] = getDefaultConfigValue(val)
}
})
return value
} else {
return getDefaultObject(configSpec)
}
}
export function mapObjectSpec (spec: ValueSpecObject, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return mapConfigSpec(spec.spec, value)
} else {
return null
}
}
export function mapUnionSpec (spec: ValueSpecUnion, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const variant = mapEnumSpec({
...spec.tag,
type: 'enum',
default: spec.default,
values: Object.keys(spec.variants),
valueNames: spec.tag.variantNames,
}, value[spec.tag.id])
value = mapConfigSpec(spec.variants[variant], value)
value[spec.tag.id] = variant
return value
} else {
return getDefaultUnion(spec)
}
}
export function mapStringSpec (value: any): string {
if (typeof value === 'string') {
return value
} else {
return null
}
}
export function mapNumberSpec (value: any): number {
if (typeof value === 'number') {
return value
} else {
return null
}
}
export function mapEnumSpec (spec: ValueSpecEnum, value: any): string {
if (typeof value === 'string' && spec.values.includes(value)) {
return value
} else {
return spec.default
}
}
export function mapListSpec (spec: ValueSpecList, value: any): string[] | number[] | object[] {
if (Array.isArray(value)) {
const innerSpec = listInnerSpec(spec)
return value.map(item => mapSpecToConfigValue(innerSpec, item))
} else {
return getDefaultList(spec)
}
}
export function mapBooleanSpec (spec: ValueSpecBoolean, value: any): boolean {
if (typeof value === 'boolean') {
return value
} else {
return spec.default
}
}
export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null {
switch (spec.type) {
case 'object':
return spec.nullByDefault ? null : getDefaultObject(spec.spec)
case 'union':
return getDefaultUnion(spec)
case 'string':
return spec.default ? getDefaultString(spec.default) : null
case 'number':
return spec.default || null
case 'list':
return getDefaultList(spec)
case 'enum':
case 'boolean':
return spec.default
case 'pointer':
return null
}
}
export function getDefaultObject (spec: ConfigSpec): object {
const obj = { }
Object.entries(spec).map(([key, val]) => {
obj[key] = getDefaultConfigValue(val)
})
return obj
}
export function getDefaultList (spec: ValueSpecList): string[] | number[] | object[] {
if (spec.subtype === 'object') {
const l = (spec.default as any[])
const range = Range.from(spec.range)
while (l.length < range.integralMin()) {
l.push(getDefaultConfigValue(listInnerSpec(spec)))
}
return l as string[] | number[] | object[]
} else {
const l = (spec.default as any[]).map(d => getDefaultConfigValue({ ...listInnerSpec(spec), default: d }))
return l as string[] | number[] | object[]
}
}
export function getDefaultUnion (spec: ValueSpecUnion): object {
return { [spec.tag.id]: spec.default, ...getDefaultObject(spec.variants[spec.default]) }
}
export function getDefaultMapTagKey (defaultSpec: DefaultString = '', value: object): string {
const keySrc = getDefaultString(defaultSpec)
const keys = Object.keys(value)
let key = keySrc
let idx = 1
while (keys.includes(key)) {
key = `${keySrc}-${idx++}`
}
return key
}
export function getDefaultString (defaultSpec: DefaultString): string {
if (typeof defaultSpec === 'string') {
return defaultSpec
} else {
let s = ''
for (let i = 0; i < defaultSpec.len; i++) {
s = s + getRandomCharInSet(defaultSpec.charset)
}
return s
}
}
export function getDefaultDescription (spec: ValueSpec): string {
let toReturn: string | undefined
switch (spec.type) {
case 'string':
if (typeof spec.default === 'string') {
toReturn = spec.default
} else if (typeof spec.default === 'object') {
toReturn = 'random'
}
break
case 'number':
if (typeof spec.default === 'number') {
toReturn = String(spec.default)
}
break
case 'boolean':
toReturn = spec.default === true ? 'True' : 'False'
break
case 'enum':
toReturn = spec.valueNames[spec.default]
break
}
return toReturn || ''
}
// a,g,h,A-Z,,,,-
export function getRandomCharInSet (charset: string): string {
const set = stringToCharSet(charset)
let charIdx = Math.floor(Math.random() * set.len)
for (let range of set.ranges) {
if (range.len > charIdx) {
return String.fromCharCode(range.start.charCodeAt(0) + charIdx)
}
charIdx -= range.len
}
throw new Error('unreachable')
}
function stringToCharSet (charset: string): CharSet {
let set: CharSet = { ranges: [], len: 0 }
let start: string | null = null
let end: string | null = null
let in_range = false
for (let char of charset) {
switch (char) {
case ',':
if (start !== null && end !== null) {
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
throw new Error('start > end of charset')
}
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
set.ranges.push({
start,
end,
len,
})
set.len += len
start = null
end = null
in_range = false
} else if (start !== null && !in_range) {
set.len += 1
set.ranges.push({ start, end: start, len: 1 })
start = null
} else if (start !== null && in_range) {
end = ','
} else if (start === null && end === null && !in_range) {
start = ','
} else {
throw new Error('unexpected ","')
}
break
case '-':
if (start === null) {
start = '-'
} else if (!in_range) {
in_range = true
} else if (in_range && end === null) {
end = '-'
} else {
throw new Error('unexpected "-"')
}
break
default:
if (start === null) {
start = char
} else if (in_range && end === null) {
end = char
} else {
throw new Error(`unexpected "${char}"`)
}
}
}
if (start !== null && end !== null) {
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
throw new Error('start > end of charset')
}
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
set.ranges.push({
start,
end,
len,
})
set.len += len
} else if (start !== null) {
set.len += 1
set.ranges.push({
start,
end: start,
len: 1,
})
}
return set
}
interface CharSet {
ranges: {
start: string
end: string
len: number
}[]
len: number
}

View File

@@ -0,0 +1,27 @@
import { ConfigCursor } from './config-cursor'
import { TrackingModalController } from '../services/tracking-modal-controller.service'
export class ModalPresentable {
constructor (private readonly trackingModalCtrl: TrackingModalController) { }
async presentModal (cursor: ConfigCursor<any>, callback: () => any) {
const modal = await this.trackingModalCtrl.createConfigModal({
backdropDismiss: false,
presentingElement: await this.trackingModalCtrl.getTop(),
componentProps: {
cursor,
},
}, cursor.spec().type)
modal.onWillDismiss().then(res => {
cursor.injectModalData(res)
callback()
})
await modal.present()
}
dismissModal (a: any) {
return this.trackingModalCtrl.dismiss(a)
}
}