codecombat/app/schemas/schemas.coffee

263 lines
10 KiB
CoffeeScript

#language imports
Language = require './languages'
# schema helper methods
me = module.exports
combine = (base, ext) ->
return base unless ext?
return _.extend(base, ext)
urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_=]*)?$'
# Common schema properties
me.object = (ext, props) -> combine({type: 'object', additionalProperties: false, properties: props or {}}, ext)
me.array = (ext, items) -> combine({type: 'array', items: items or {}}, ext)
me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext)
me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
# Dates should usually be strings, ObjectIds should be strings: https://github.com/codecombat/codecombat/issues/1384
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # old
me.stringDate = (ext) -> combine({type: ['string'], format: 'date-time'}, ext) # new
me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext) # old
me.stringID = (ext) -> schema = combine({type: 'string', minLength: 24, maxLength: 24}, ext) # use for anything new
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
me.int = (ext) -> combine {type: 'integer'}, ext
me.float = (ext) -> combine {type: 'number'}, ext
PointSchema = me.object {title: 'Point', description: 'An {x, y} coordinate point.', format: 'point2d', required: ['x', 'y']},
x: {title: 'x', description: 'The x coordinate.', type: 'number', 'default': 15}
y: {title: 'y', description: 'The y coordinate.', type: 'number', 'default': 20}
me.point2d = (ext) -> combine(_.cloneDeep(PointSchema), ext)
SoundSchema = me.object {format: 'sound'},
mp3: {type: 'string', format: 'sound-file'}
ogg: {type: 'string', format: 'sound-file'}
me.sound = (props) ->
obj = _.cloneDeep(SoundSchema)
obj.properties[prop] = props[prop] for prop of props
obj
ColorConfigSchema = me.object {format: 'color-sound'},
hue: {format: 'range', type: 'number', minimum: 0, maximum: 1}
saturation: {format: 'range', type: 'number', minimum: 0, maximum: 1}
lightness: {format: 'range', type: 'number', minimum: 0, maximum: 1}
me.colorConfig = (props) ->
obj = _.cloneDeep(ColorConfigSchema)
obj.properties[prop] = props[prop] for prop of props
obj
# BASICS
basicProps = (linkFragment) ->
_id: me.objectId(links: [{rel: 'self', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden')
__v: {title: 'Mongoose Version', format: 'hidden'}
me.extendBasicProperties = (schema, linkFragment) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, basicProps(linkFragment))
# PATCHABLE
patchableProps = ->
patches: me.array({title:'Patches'}, {
_id: me.objectId(links: [{rel: 'db', href: '/db/patch/{($)}'}], title: 'Patch ID', description: 'A reference to the patch.')
status: {enum: ['pending', 'accepted', 'rejected', 'cancelled']}
})
allowPatches: {type: 'boolean'}
watchers: me.array({title: 'Watchers'},
me.objectId(links: [{rel: 'extra', href: '/db/user/{($)}'}]))
me.extendPatchableProperties = (schema) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, patchableProps())
# NAMED
namedProps = ->
name: me.shortString({title: 'Name'})
slug: me.shortString({title: 'Slug', format: 'hidden'})
me.extendNamedProperties = (schema) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, namedProps())
# VERSIONED
versionedProps = (linkFragment) ->
version:
'default': {minor: 0, major: 0, isLatestMajor: true, isLatestMinor: true}
format: 'version'
title: 'Version'
type: 'object'
readOnly: true
additionalProperties: false
properties:
major: {type: 'number', minimum: 0}
minor: {type: 'number', minimum: 0}
isLatestMajor: {type: 'boolean'}
isLatestMinor: {type: 'boolean'}
# TODO: figure out useful 'rel' values here
original: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden')
parent: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden')
creator: me.objectId(links: [{rel: 'extra', href: '/db/user/{($)}'}], format: 'hidden')
created: me.date({title: 'Created', readOnly: true})
commitMessage: {type: 'string', maxLength: 500, title: 'Commit Message', readOnly: true}
me.extendVersionedProperties = (schema, linkFragment) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, versionedProps(linkFragment))
# SEARCHABLE
searchableProps = ->
index: {format: 'hidden'}
me.extendSearchableProperties = (schema) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, searchableProps())
# PERMISSIONED
permissionsProps = ->
permissions:
type: 'array'
items:
type: 'object'
additionalProperties: false
properties:
target: {}
access: {type: 'string', 'enum': ['read', 'write', 'owner']}
format: 'hidden'
me.extendPermissionsProperties = (schema) ->
schema.properties = {} unless schema.properties?
_.extend(schema.properties, permissionsProps())
# TRANSLATABLE
me.generateLanguageCodeArrayRegex = -> '^(' + Language.languageCodes.join('|') + ')$'
me.getLanguageCodeArray = ->
return Language.languageCodes
me.getLanguagesObject = -> return Language
me.extendTranslationCoverageProperties = (schema) ->
schema.properties = {} unless schema.properties?
schema.properties.i18nCoverage = { title: 'i18n Coverage', type: 'array', items: { type: 'string' }}
# OTHER
me.classNamePattern = '^[A-Z][A-Za-z0-9]*$' # starts with capital letter; just letters and numbers
me.identifierPattern = '^[a-z][A-Za-z0-9]*$' # starts with lowercase letter; just letters and numbers
me.constantPattern = '^[A-Z0-9_]+$' # just uppercase letters, underscores, and numbers
me.identifierOrConstantPattern = '^([a-z][A-Za-z0-9]*|[A-Z0-9_]+)$'
me.FunctionArgumentSchema = me.object {
title: 'Function Argument',
description: 'Documentation entry for a function argument.'
'default':
name: 'target'
type: 'object'
example: 'this.getNearestEnemy()'
description: 'The target of this function.'
required: ['name', 'type', 'example', 'description']
},
name: {type: 'string', pattern: me.identifierPattern, title: 'Name', description: 'Name of the function argument.'}
i18n: { type: 'object', format: 'i18n', props: ['description'], description: 'Help translate this argument'}
# not actual JS types, just whatever they describe...
type: me.shortString(title: 'Type', description: 'Intended type of the argument.')
example:
oneOf: [
{
type: 'object',
title: 'Language Examples',
description: 'Examples by code language.',
additionalProperties: me.shortString(description: 'Example value for the argument.')
format: 'code-languages-object'
default: {javascript: '', python: ''}
}
me.shortString(title: 'Example', description: 'Example value for the argument.')
]
description:
oneOf: [
{
type: 'object',
title: 'Language Descriptions',
description: 'Example argument descriptions by code language.',
additionalProperties: {type: 'string', description: 'Description of the argument.', maxLength: 1000}
format: 'code-languages-object'
default: {javascript: '', python: ''}
}
{title: 'Description', type: 'string', description: 'Description of the argument.', maxLength: 1000}
]
'default':
title: 'Default'
description: 'Default value of the argument. (Your code should set this.)'
'default': null
me.codeSnippet = me.object {description: 'A language-specific code snippet'},
code: {type: 'string', format: 'code', title: 'Snippet', default: '', description: 'Code snippet. Use ${1:defaultValue} syntax to add flexible arguments'}
tab: {type: 'string', title: 'Tab Trigger', description: 'Tab completion text. Will be expanded to the snippet if typed and hit tab.'}
me.activity = me.object {description: 'Stats on an activity'},
first: me.date()
last: me.date()
count: {type: 'integer', minimum: 0}
me.terrainString = me.shortString {enum: ['Grass', 'Dungeon', 'Indoor', 'Desert', 'Mountain', 'Glacier', 'Volcano'], title: 'Terrain', description: 'Which terrain type this is.'}
me.HeroConfigSchema = me.object {description: 'Which hero the player is using, equipped with what inventory.'},
inventory:
type: 'object'
description: 'The inventory of the hero: slots to item ThangTypes.'
additionalProperties: me.objectId(description: 'An item ThangType.')
thangType: me.objectId(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Thang Type', description: 'The ThangType of the hero.', format: 'thang-type')
me.RewardSchema = (descriptionFragment='earned by achievements') ->
type: 'object'
additionalProperties: false
description: "Rewards #{descriptionFragment}."
properties:
heroes: me.array {uniqueItems: true, description: "Heroes #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Hero ThangType', description: 'A reference to the earned hero ThangType.', format: 'thang-type')
items: me.array {uniqueItems: true, description: "Items #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Item ThangType', description: 'A reference to the earned item ThangType.', format: 'thang-type')
levels: me.array {uniqueItems: true, description: "Levels #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/level/{($)}/version'}], title: 'Level', description: 'A reference to the earned Level.', format: 'latest-version-original-reference')
gems: me.float {description: "Gems #{descriptionFragment}."}
me.task = me.object {title: 'Task', description: 'A task to be completed', format: 'task', default: {name: 'TODO', complete: false}},
name: {title: 'Name', description: 'What must be done?', type: 'string'}
complete: {title: 'Complete', description: 'Whether this task is done.', type: 'boolean', format: 'checkbox'}
me.concept = me.shortString enum: [
'advanced_strings'
'algorithms'
'arguments'
'arithmetic'
'arrays'
'basic_syntax'
'boolean_logic'
'break_statements'
'classes'
'continue_statements'
'for_loops'
'functions'
'graphics'
'if_statements'
'input_handling'
'math_operations'
'object_literals'
'parameters'
'strings'
'variables'
'vectors'
'while_loops'
'recursion'
]