From 6fb5b59a012389cc607c3e6e2256adfb31f42130 Mon Sep 17 00:00:00 2001 From: Aditya Raisinghani Date: Sat, 12 Apr 2014 14:05:56 +0530 Subject: [PATCH] Refactored schemas to be in /app --- app/models/CocoModel.coffee | 21 +- app/models/SuperModel.coffee | 4 +- app/schemas/article_schema.coffee | 13 + app/schemas/i18n_schema.coffee | 48 ++++ app/schemas/languages.coffee | 49 ++++ app/schemas/level_component_schema.coffee | 119 ++++++++ app/schemas/level_feedback_schema.coffee | 27 ++ app/schemas/level_schema.coffee | 254 ++++++++++++++++++ app/schemas/level_session_schema.coffee | 213 +++++++++++++++ app/schemas/level_system_schema.coffee | 106 ++++++++ app/schemas/metaschema.coffee | 132 +++++++++ app/schemas/schemas.coffee | 158 +++++++++++ app/schemas/thang_component_schema.coffee | 21 ++ app/schemas/thang_type_schema.coffee | 153 +++++++++++ app/schemas/user_schema.coffee | 98 +++++++ app/views/account/settings_view.coffee | 4 +- app/views/editor/article/edit.coffee | 2 +- app/views/editor/components/main.coffee | 4 +- app/views/editor/level/component/edit.coffee | 6 +- .../editor/level/scripts_tab_view.coffee | 4 +- .../editor/level/settings_tab_view.coffee | 2 +- app/views/editor/level/system/edit.coffee | 6 +- .../editor/level/systems_tab_view.coffee | 2 +- app/views/editor/level/thangs_tab_view.coffee | 2 +- app/views/editor/thang/colors_tab_view.coffee | 4 +- app/views/editor/thang/edit.coffee | 4 +- app/views/modal/login_modal.coffee | 2 +- app/views/modal/signup_modal.coffee | 2 +- server/routes/db.coffee | 4 +- 29 files changed, 1429 insertions(+), 35 deletions(-) create mode 100644 app/schemas/article_schema.coffee create mode 100644 app/schemas/i18n_schema.coffee create mode 100644 app/schemas/languages.coffee create mode 100644 app/schemas/level_component_schema.coffee create mode 100644 app/schemas/level_feedback_schema.coffee create mode 100644 app/schemas/level_schema.coffee create mode 100644 app/schemas/level_session_schema.coffee create mode 100644 app/schemas/level_system_schema.coffee create mode 100644 app/schemas/metaschema.coffee create mode 100644 app/schemas/schemas.coffee create mode 100644 app/schemas/thang_component_schema.coffee create mode 100644 app/schemas/thang_type_schema.coffee create mode 100644 app/schemas/user_schema.coffee diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 8dc8e03a1..974cd4826 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -5,9 +5,11 @@ auth = require 'lib/auth' class CocoSchema extends Backbone.Model constructor: (path, args...) -> super(args...) - @urlRoot = path + '/schema' + # @urlRoot = path + '/schema' + @schemaName = path[4..].replace '.', '_' + @schema = require 'schemas/' + @schemaName + '_schema' -window.CocoSchema = CocoSchema +# window.CocoSchema = CocoSchema.schema class CocoModel extends Backbone.Model idAttribute: "_id" @@ -18,7 +20,7 @@ class CocoModel extends Backbone.Model initialize: -> super() - @constructor.schema ?= new CocoSchema(@urlRoot) + @constructor.schema ?= @urlRoot[4..].replace '.', '_' if not @constructor.className console.error("#{@} needs a className set.") @markToRevert() @@ -65,8 +67,9 @@ class CocoModel extends Backbone.Model loadSchema: -> return if @constructor.schema.loading - @constructor.schema.fetch() - @listenToOnce(@constructor.schema, 'sync', @onConstructorSync) + @constructor.schema = require 'schemas/' + @constructor.schema + '_schema' unless @constructor.schema.loaded + @onConstructorSync() + # @listenToOnce(@constructor.schema, 'sync', @onConstructorSync) onConstructorSync: -> @constructor.schema.loaded = true @@ -77,7 +80,7 @@ class CocoModel extends Backbone.Model schema: -> return @constructor.schema validate: -> - result = tv4.validateMultiple(@attributes, @constructor.schema?.attributes or {}) + result = tv4.validateMultiple(@attributes, @constructor.schema? or {}) if result.errors?.length console.log @, "got validate result with errors:", result return result.errors unless result.valid @@ -138,11 +141,11 @@ class CocoModel extends Backbone.Model addSchemaDefaults: -> return if @addedSchemaDefaults or not @constructor.hasSchema() @addedSchemaDefaults = true - for prop, defaultValue of @constructor.schema.attributes.default or {} + for prop, defaultValue of @constructor.schema.default or {} continue if @get(prop)? #console.log "setting", prop, "to", defaultValue, "from attributes.default" @set prop, defaultValue - for prop, sch of @constructor.schema.attributes.properties or {} + for prop, sch of @constructor.schema.properties or {} continue if @get(prop)? #console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default? @set prop, sch.default if sch.default? @@ -154,7 +157,7 @@ class CocoModel extends Backbone.Model # returns unfetched model shells for every referenced doc in this model # OPTIMIZE so that when loading models, it doesn't cause the site to stutter data ?= @attributes - schema ?= @schema().attributes + schema ?= @schema() models = [] if $.isArray(data) and schema.items? diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 9e2bcd347..adcac62eb 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -29,9 +29,9 @@ class SuperModel model.loadSchema() schema = model.schema() unless schema.loaded - @schemas[schema.urlRoot] = schema + @schemas[model.urlRoot] = schema return schema.once('sync', => @modelLoaded(model)) - refs = model.getReferencedModels(model.attributes, schema.attributes, '/', @shouldLoadProjection) + refs = model.getReferencedModels(model.attributes, schema, '/', @shouldLoadProjection) refs = [] unless @mustPopulate is model or @shouldPopulate(model) # console.log 'Loaded', model.get('name') for ref, i in refs when @shouldLoadReference ref diff --git a/app/schemas/article_schema.coffee b/app/schemas/article_schema.coffee new file mode 100644 index 000000000..012d46ec3 --- /dev/null +++ b/app/schemas/article_schema.coffee @@ -0,0 +1,13 @@ +c = require './schemas' + +ArticleSchema = c.object() +c.extendNamedProperties ArticleSchema # name first + +ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } +ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body'] } + +c.extendBasicProperties(ArticleSchema, 'article') +c.extendSearchableProperties(ArticleSchema) +c.extendVersionedProperties(ArticleSchema, 'article') + +module.exports = ArticleSchema diff --git a/app/schemas/i18n_schema.coffee b/app/schemas/i18n_schema.coffee new file mode 100644 index 000000000..2a2aaf816 --- /dev/null +++ b/app/schemas/i18n_schema.coffee @@ -0,0 +1,48 @@ +#this file will hold the experimental JSON schema for i18n +c = require './schemas' + +languageCodeArrayRegex = c.generateLanguageCodeArrayRegex() + + +ExampleSchema = { + title: "Example Schema", + description:"An example schema", + type: "object", + properties: { + text: { + title: "Text", + description: "A short message to display in the dialogue area. Markdown okay.", + type: "string", + maxLength: 400 + }, + i18n: {"$ref": "#/definitions/i18n"} + }, + + definitions: { + i18n: { + title: "i18n", + description: "The internationalization object", + type: "object", + patternProperties: { + languageCodeArrayRegex: { + additionalProperties: false, + properties: { + #put the translatable properties here + #if it is possible to not include i18n with a reference + # to #/properties, you could just do + properties: {"$ref":"#/properties"} + # text: {"$ref": "#/properties/text"} + } + default: { + title: "LanguageCode", + description: "LanguageDescription" + } + } + } + } + }, + +} + +#define a i18n object type for each schema, then have the i18n have it's oneOf check against +#translatable schemas of that object \ No newline at end of file diff --git a/app/schemas/languages.coffee b/app/schemas/languages.coffee new file mode 100644 index 000000000..e9c8e0f33 --- /dev/null +++ b/app/schemas/languages.coffee @@ -0,0 +1,49 @@ +# errors = require '../commons/errors' +# log = require 'winston' +locale = require '../locale/locale' # requiring from app; will break if we stop serving from where app lives + +# module.exports.setup = (app) -> +# app.all '/languages/add/:lang/:namespace', (req, res) -> +# # Should probably store these somewhere +# log.info "#{req.params.lang}.#{req.params.namespace} missing an i18n key:", req.body +# res.send('') +# res.end() + +# app.all '/languages', (req, res) -> +# # Now that these are in the client, not sure when we would use this, but hey +# return errors.badMethod(res) if req.route.method isnt 'get' +# res.send(languages) +# return res.end() + +languages = [] +for code, localeInfo of locale + languages.push code: code, nativeDescription: localeInfo.nativeDescription, englishDescription: localeInfo.englishDescription + +module.exports.languages = languages +module.exports.languageCodes = languageCodes = (language.code for language in languages) +module.exports.languageCodesLower = languageCodesLower = (code.toLowerCase() for code in languageCodes) + +# Keep keys lower-case for matching and values with second subtag uppercase like i18next expects +languageAliases = + 'en': 'en-US' + + 'zh-cn': 'zh-HANS' + 'zh-hans-cn': 'zh-HANS' + 'zh-sg': 'zh-HANS' + 'zh-hans-sg': 'zh-HANS' + + 'zh-tw': 'zh-HANT' + 'zh-hant-tw': 'zh-HANT' + 'zh-hk': 'zh-HANT' + 'zh-hant-hk': 'zh-HANT' + 'zh-mo': 'zh-HANT' + 'zh-hant-mo': 'zh-HANT' + +module.exports.languageCodeFromAcceptedLanguages = languageCodeFromAcceptedLanguages = (acceptedLanguages) -> + for lang in acceptedLanguages ? [] + code = languageAliases[lang.toLowerCase()] + return code if code + codeIndex = _.indexOf languageCodesLower, lang + if codeIndex isnt -1 + return languageCodes[codeIndex] + return 'en-US' diff --git a/app/schemas/level_component_schema.coffee b/app/schemas/level_component_schema.coffee new file mode 100644 index 000000000..d67c69376 --- /dev/null +++ b/app/schemas/level_component_schema.coffee @@ -0,0 +1,119 @@ +c = require './schemas' +metaschema = require './metaschema' + +attackSelfCode = """ +class AttacksSelf extends Component + @className: "AttacksSelf" + chooseAction: -> + @attack @ +""" +systems = [ + 'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing' + 'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect', + 'magic' +] + +PropertyDocumentationSchema = c.object { + title: "Property Documentation" + description: "Documentation entry for a property this Component will add to its Thang which other Components might + want to also use." + "default": + name: "foo" + type: "object" + description: 'The `foo` property can satisfy all the #{spriteName}\'s foobar needs. Use it wisely.' + required: ['name', 'type', 'description'] +}, + name: {type: 'string', title: "Name", description: "Name of the property."} + # not actual JS types, just whatever they describe... + type: c.shortString(title: "Type", description: "Intended type of the property.") + description: {title: "Description", type: 'string', description: "Description of the property.", format: 'markdown', maxLength: 1000} + args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema + owner: {title: "Owner", type: 'string', description: 'Owner of the property, like "this" or "Math".'} + example: {title: "Example", type: 'string', description: 'An optional example code block.', format: 'javascript'} + returns: c.object { + title: "Return Value" + description: 'Optional documentation of any return value.' + required: ['type'] + default: {type: 'null'} + }, + type: c.shortString(title: "Type", description: "Type of the return value") + example: c.shortString(title: "Example", description: "Example return value") + description: {title: "Description", type: 'string', description: "Description of the return value.", maxLength: 1000} + +DependencySchema = c.object { + title: "Component Dependency" + description: "A Component upon which this Component depends." + "default": + #original: ? + majorVersion: 0 + required: ["original", "majorVersion"] + format: 'latest-version-reference' + links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to another Component upon which this Component depends.") + majorVersion: + title: "Major Version" + description: "Which major version of the Component this Component needs." + type: 'integer' + minimum: 0 + +LevelComponentSchema = c.object { + title: "Component" + description: "A Component which can affect Thang behavior." + required: ["system", "name", "description", "code", "dependencies", "propertyDocumentation", "language"] + "default": + system: "ai" + name: "AttacksSelf" + description: "This Component makes the Thang attack itself." + code: attackSelfCode + language: "coffeescript" + dependencies: [] # TODO: should depend on something by default + propertyDocumentation: [] +} +c.extendNamedProperties LevelComponentSchema # let's have the name be the first property +LevelComponentSchema.properties.name.pattern = c.classNamePattern +_.extend LevelComponentSchema.properties, + system: + title: "System" + description: "The short name of the System this Component belongs to, like \"ai\"." + type: "string" + "enum": systems + "default": "ai" + description: + title: "Description" + description: "A short explanation of what this Component does." + type: "string" + maxLength: 2000 + "default": "This Component makes the Thang attack itself." + language: + type: "string" + title: "Language" + description: "Which programming language this Component is written in." + "enum": ["coffeescript"] + code: + title: "Code" + description: "The code for this Component, as a CoffeeScript class. TODO: add link to documentation for + how to write these." + "default": attackSelfCode + type: "string" + format: "coffee" + js: + title: "JavaScript" + description: "The transpiled JavaScript code for this Component" + type: "string" + format: "hidden" + dependencies: c.array {title: "Dependencies", description: "An array of Components upon which this Component depends.", "default": [], uniqueItems: true}, DependencySchema + propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this Component will add to its Thang which other Components might want to also use.", "default": []}, PropertyDocumentationSchema + configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this Component as configuration.", default: {type: 'object', additionalProperties: false}} + official: + type: "boolean" + title: "Official" + description: "Whether this is an official CodeCombat Component." + "default": false + +c.extendBasicProperties LevelComponentSchema, 'level.component' +c.extendSearchableProperties LevelComponentSchema +c.extendVersionedProperties LevelComponentSchema, 'level.component' +c.extendPermissionsProperties LevelComponentSchema, 'level.component' + +module.exports = LevelComponentSchema diff --git a/app/schemas/level_feedback_schema.coffee b/app/schemas/level_feedback_schema.coffee new file mode 100644 index 000000000..201beb468 --- /dev/null +++ b/app/schemas/level_feedback_schema.coffee @@ -0,0 +1,27 @@ +c = require './schemas' + +LevelFeedbackLevelSchema = c.object {required: ['original', 'majorVersion']}, { + original: c.objectId({}) + majorVersion: {type: 'integer', minimum: 0, default: 0}} + +LevelFeedbackSchema = c.object { + title: "Feedback" + description: "Feedback on a level." +} + +_.extend LevelFeedbackSchema.properties, + # denormalization + creatorName: { type: 'string' } + levelName: { type: 'string' } + levelID: { type: 'string' } + + creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) + created: c.date( { title: 'Created', readOnly: true }) + + level: LevelFeedbackLevelSchema + rating: { type: 'number', minimum: 1, maximum: 5 } + review: { type: 'string' } + +c.extendBasicProperties LevelFeedbackSchema, 'level.feedback' + +module.exports = LevelFeedbackSchema diff --git a/app/schemas/level_schema.coffee b/app/schemas/level_schema.coffee new file mode 100644 index 000000000..e372bdd52 --- /dev/null +++ b/app/schemas/level_schema.coffee @@ -0,0 +1,254 @@ +c = require './schemas' +ThangComponentSchema = require './thang_component_schema' + +SpecificArticleSchema = c.object() +c.extendNamedProperties SpecificArticleSchema # name first +SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' } +SpecificArticleSchema.displayProperty = 'name' + +side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']} +thang = {title: "Thang", description: "The name of a Thang.", type: 'string', maxLength: 30, format:'thang'} + +eventPrereqValueTypes = ["boolean", "integer", "number", "null", "string"] # not "object" or "array" +EventPrereqSchema = c.object {title: "Event Prerequisite", format: 'event-prereq', description: "Script requires that the value of some property on the event triggering it to meet some prerequisite.", "default": {eventProps: []}, required: ["eventProps"]}, + eventProps: c.array {'default': ["thang"], format:'event-value-chain', maxItems: 10, title: "Event Property", description: 'A chain of keys in the event, like "thang.pos.x" to access event.thang.pos.x.'}, c.shortString(title: "Property", description: "A key in the event property key chain.") + equalTo: c.object {type: eventPrereqValueTypes, title: "==", description: "Script requires the event's property chain value to be equal to this value."} + notEqualTo: c.object {type: eventPrereqValueTypes, title: "!=", description: "Script requires the event's property chain value to *not* be equal to this value."} + greaterThan: {type: 'number', title: ">", description: "Script requires the event's property chain value to be greater than this value."} + greaterThanOrEqualTo: {type: 'number', title: ">=", description: "Script requires the event's property chain value to be greater or equal to this value."} + lessThan: {type: 'number', title: "<", description: "Script requires the event's property chain value to be less than this value."} + lessThanOrEqualTo: {type: 'number', title: "<=", description: "Script requires the event's property chain value to be less than or equal to this value."} + containingString: c.shortString(title: "Contains", description: "Script requires the event's property chain value to be a string containing this string.") + notContainingString: c.shortString(title: "Does not contain", description: "Script requires the event's property chain value to *not* be a string containing this string.") + containingRegexp: c.shortString(title: "Contains Regexp", description: "Script requires the event's property chain value to be a string containing this regular expression.") + notContainingRegexp: c.shortString(title: "Does not contain regexp", description: "Script requires the event's property chain value to *not* be a string containing this regular expression.") + +GoalSchema = c.object {title: "Goal", description: "A goal that the player can accomplish.", required: ["name", "id"]}, + name: c.shortString(title: "Name", description: "Name of the goal that the player will see, like \"Defeat eighteen dragons\".") + i18n: {type: "object", format: 'i18n', props: ['name'], description: "Help translate this goal"} + id: c.shortString(title: "ID", description: "Unique identifier for this goal, like \"defeat-dragons\".") # unique somehow? + worldEndsAfter: {title: 'World Ends After', description: "When included, ends the world this many seconds after this goal succeeds or fails.", type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 300, default: 3} + howMany: {title: "How Many", description: "When included, require only this many of the listed goal targets instead of all of them.", type: 'integer', minimum: 1} + hiddenGoal: {title: "Hidden", description: "Hidden goals don't show up in the goals area for the player until they're failed. (Usually they're obvious, like 'don't die'.)", 'type': 'boolean', default: false} + team: c.shortString(title: 'Team', description: 'Name of the team this goal is for, if it is not for all of the playable teams.') + killThangs: c.array {title: "Kill Thangs", description: "A list of Thang IDs the player should kill, or team names.", uniqueItems: true, minItems: 1, "default": ["ogres"]}, thang + saveThangs: c.array {title: "Save Thangs", description: "A list of Thang IDs the player should save, or team names", uniqueItems: true, minItems: 1, "default": ["humans"]}, thang + getToLocations: c.object {title: "Get To Locations", description: "Will be set off when any of the \"who\" touch any of the \"targets\" ", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang + getAllToLocations: c.array {title: "Get all to locations", description: "Similar to getToLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect",required: ["getToLocation"]}, + c.object {title: "", description: ""}, + getToLocation: c.object {title: "Get To Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang + keepFromLocations: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang + keepAllFromLocations: c.array {title: "Keep ALL From Locations", description: "Similar to keepFromLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect", required: ["keepFromLocation"]}, + c.object {title: "", description: ""}, + keepFromLocation: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang + leaveOffSides: c.object {title: "Leave Off Sides", description: "Sides of the level to get some Thangs to leave across.", required: ["who", "sides"]}, + who: c.array {title: "Who", description: "The Thangs which must leave off the sides of the level.", minItems: 1}, thang + sides: c.array {title: "Sides", description: "The sides off which the Thangs must leave.", minItems: 1}, side + keepFromLeavingOffSides: c.object {title: "Keep From Leaving Off Sides", description: "Sides of the level to keep some Thangs from leaving across.", required: ["who", "sides"]}, + who: c.array {title: "Who", description: "The Thangs which must not leave off the sides of the level.", minItems: 1}, thang + sides: side, {title: "Sides", description: "The sides off which the Thangs must not leave.", minItems: 1}, side + collectThangs: c.object {title: "Collect", description: "Thangs that other Thangs must collect.", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs which must collect the target items.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target items which the Thangs must collect.", minItems: 1}, thang + keepFromCollectingThangs: c.object {title: "Keep From Collecting", description: "Thangs that the player must prevent other Thangs from collecting.", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs which must not collect the target items.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target items which the Thangs must not collect.", minItems: 1}, thang + +ResponseSchema = c.object {title: "Dialogue Button", description: "A button to be shown to the user with the dialogue.", required: ["text"]}, + text: {title: "Title", description: "The text that will be on the button", "default": "Okay", type: 'string', maxLength: 30} + channel: c.shortString(title: "Channel", format: 'event-channel', description: 'Channel that this event will be broadcast over, like "level-set-playing".') + event: {type: 'object', title: "Event", description: "Event that will be broadcast when this button is pressed, like {playing: true}."} + buttonClass: c.shortString(title: "Button Class", description: 'CSS class that will be added to the button, like "btn-primary".') + i18n: {type: "object", format: 'i18n', props: ['text'], description: "Help translate this button"} + +PointSchema = c.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} + +SpriteCommandSchema = c.object {title: "Thang Command", description: "Make a target Thang move or say something, or select/deselect it.", required: ["id"], default: {id: "Captain Anya"}}, + id: thang + select: {title: "Select", description: "Select or deselect this Thang.", type: 'boolean'} + say: c.object {title: "Say", description: "Make this Thang say a message.", required: ["text"]}, + blurb: c.shortString(title: "Blurb", description: "A very short message to display above this Thang's head. Plain text.", maxLength: 50) + mood: c.shortString(title: "Mood", description: "The mood with which the Thang speaks.", "enum": ["explain", "debrief", "congrats", "attack", "joke", "tip", "alarm"], "default": "explain") + text: {title: "Text", description: "A short message to display in the dialogue area. Markdown okay.", type: "string", maxLength: 400} + sound: c.object {title: "Sound", description: "A dialogue sound file to accompany the message.", required: ["mp3", "ogg"]}, + mp3: c.shortString(title: "MP3", format: 'sound-file') + ogg: c.shortString(title: "OGG", format: 'sound-file') + preload: {title: "Preload", description: "Whether to load this sound file before the level can begin (typically for the first dialogue of a level).", type: 'boolean', "default": false} + responses: c.array {title: "Buttons", description: "An array of buttons to include with the dialogue, with which the user can respond."}, ResponseSchema + i18n: {type: "object", format: 'i18n', props: ['blurb', 'text'], description: "Help translate this message"} + move: c.object {title: "Move", description: "Tell the Thang to move.", required: ['target'], default: {target: {x: 20, y: 20}, duration: 500}}, + target: _.extend _.cloneDeep(PointSchema), {title: 'Target', description: 'Target point to which the Thang will move.'} + duration: {title: "Duration", description: "Number of milliseconds over which to move, or 0 for an instant move.", type: 'integer', minimum: 0, default: 500, format: 'milliseconds'} + +NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes that should be sent out as a result of this script triggering.", displayProperty: "name"}, + name: {title: "Name", description: "Short name describing the script, like \"Anya greets the player\", for your convenience.", type: "string"} + dom: c.object {title: "DOM", description: "Manipulate things in the play area DOM, outside of the level area canvas."}, + focus: c.shortString(title: "Focus", description: "Set the window focus to this DOM selector string.") + showVictory: { + title: "Show Victory", + description: "Show the done button and maybe also the victory modal.", + enum: [true, 'Done Button', 'Done Button And Modal'] # deprecate true, same as 'done_button_and_modal' + } + highlight: c.object {title: "Highlight", description: "Highlight the target DOM selector string with a big arrow."}, + target: c.shortString(title: "Target", description: "Target highlight element DOM selector string.") + delay: {type: 'integer', minimum: 0, title: "Delay", description: "Show the highlight after this many milliseconds. Doesn't affect the dim shade cutout highlight method."} + offset: _.extend _.cloneDeep(PointSchema), {title: 'Offset', description: 'Pointing arrow tip offset in pixels from the default target.', format: null} + rotation: {type: 'number', minimum: 0, title: "Rotation", description: "Rotation of the pointing arrow, in radians. PI / 2 points left, PI points up, etc."} + sides: c.array {title: "Sides", description: "Which sides of the target element to point at."}, {type: 'string', 'enum': ['left', 'right', 'top', 'bottom'], title: "Side", description: "A side of the target element to point at."} + lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} + letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} + + goals: c.object {title: "Goals", description: "Add or remove goals for the player to complete in the level."}, + add: c.array {title: "Add", description: "Add these goals."}, GoalSchema + remove: c.array {title: "Remove", description: "Remove these goals."}, GoalSchema + + playback: c.object {title: "Playback", description: "Control the playback of the level."}, + playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} + scrub: c.object {title: "Scrub", description: "Scrub the level playback time to a certain point.", default: {offset: 2, duration: 1000, toRatio: 0.5}}, + offset: {type: 'integer', title: "Offset", description: "Number of frames by which to adjust the scrub target time.", default: 2} + duration: {type: 'integer', title: "Duration", description: "Number of milliseconds over which to scrub time.", minimum: 0, format: 'milliseconds'} + toRatio: {type: 'number', title: "To Progress Ratio", description: "Set playback time to a target playback progress ratio.", minimum: 0, maximum: 1} + toTime: {type: 'number', title: "To Time", description: "Set playback time to a target playback point, in seconds.", minimum: 0} + toGoal: c.shortString(title: "To Goal", description: "Set playback time to when this goal was achieved. (TODO: not implemented.)") + + script: c.object {title: "Script", description: "Extra configuration for this action group."}, + duration: {type: 'integer', minimum: 0, title: "Duration", description: "How long this script should last in milliseconds. 0 for indefinite.", format: 'milliseconds'} + skippable: {type: 'boolean', title: "Skippable", description: "Whether this script shouldn't bother firing when the player skips past all current scripts."} + beforeLoad: {type: 'boolean', title: "Before Load", description: "Whether this script should fire before the level is finished loading."} + + sprites: c.array {title: "Sprites", description: "Commands to issue to Sprites on the Surface."}, SpriteCommandSchema + + surface: c.object {title: "Surface", description: "Commands to issue to the Surface itself."}, + focus: c.object {title: "Camera", description: "Focus the camera on a specific point on the Surface.", format:'viewport'}, + target: {anyOf: [PointSchema, thang, {type: 'null'}], title: "Target", description: "Where to center the camera view."} + zoom: {type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 64, title: "Zoom", description: "What zoom level to use."} + duration: {type:'number', minimum: 0, title: "Duration", description: "in ms"} + bounds: c.array {title:'Boundary', maxItems: 2, minItems: 2, default:[{x:0,y:0}, {x:46, y:39}], format: 'bounds'}, PointSchema + isNewDefault: {type:'boolean', format: 'hidden', title: "New Default", description: 'Set this as new default zoom once scripts end.'} # deprecated + highlight: c.object {title: "Highlight", description: "Highlight specific Sprites on the Surface."}, + targets: c.array {title: "Targets", description: "Thang IDs of target Sprites to highlight."}, thang + delay: {type: 'integer', minimum: 0, title: "Delay", description: "Delay in milliseconds before the highlight appears."} + lockSelect: {type: 'boolean', title: "Lock Select", description: "Whether to lock Sprite selection so that the player can't select/deselect anything."} + + sound: c.object {title: "Sound", description: "Commands to control sound playback."}, + suppressSelectionSounds: {type: "boolean", title: "Suppress Selection Sounds", description: "Whether to suppress selection sounds made from clicking on Thangs."} + music: c.object { title: "Music", description: "Control music playing"}, + play: { title: "Play", type: "boolean" } + file: c.shortString(title: "File", enum:['/music/music_level_1','/music/music_level_2','/music/music_level_3','/music/music_level_4','/music/music_level_5']) + +ScriptSchema = c.object { + title: "Script" + description: 'A script fires off a chain of notes to interact with the game when a certain event triggers it.' + required: ["channel"] + 'default': {channel: "world:won", noteChain: []} +}, + id: c.shortString(title: "ID", description: "A unique ID that other scripts can rely on in their Happens After prereqs, for sequencing.") # uniqueness? + channel: c.shortString(title: "Event", format: 'event-channel', description: 'Event channel this script might trigger for, like "world:won".') + eventPrereqs: c.array {title: "Event Checks", description: "Logical checks on the event for this script to trigger.", format:'event-prereqs'}, EventPrereqSchema + repeats: {title: "Repeats", description: "Whether this script can trigger more than once during a level.", enum: [true, false, 'session'], "default": false} + scriptPrereqs: c.array {title: "Happens After", description: "Scripts that need to fire first."}, + c.shortString(title: "ID", description: "A unique ID of a script.") + notAfter: c.array {title: "Not After", description: "Do not run this script if any of these scripts have run."}, + c.shortString(title: "ID", description: "A unique ID of a script.") + noteChain: c.array {title: "Actions", description: "A list of things that happen when this script triggers."}, NoteGroupSchema + +LevelThangSchema = c.object { + title: "Thang", + description: "Thangs are any units, doodads, or abstract things that you use to build the level. (\"Thing\" was too confusing to say.)", + format: "thang" + required: ["id", "thangType", "components"] + 'default': + id: "Boris" + thangType: "Soldier" + components: [] +}, + id: thang # TODO: figure out if we can make this unique and how to set dynamic defaults + # TODO: split thangType into "original" and "majorVersion" like the rest for consistency + thangType: c.objectId(links: [{rel: "db", href: "/db/thang.type/{($)}/version"}], title: "Thang Type", description: "A reference to the original Thang template being configured.", format: 'thang-type') + components: c.array {title: "Components", description: "Thangs are configured by changing the Components attached to them.", uniqueItems: true, format: 'thang-components-array'}, ThangComponentSchema # TODO: uniqueness should be based on "original", not whole thing + +LevelSystemSchema = c.object { + title: "System" + description: "Configuration for a System that this Level uses." + format: 'level-system' + required: ['original', 'majorVersion'] + 'default': + majorVersion: 0 + config: {} + links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original System being configured.", format: "hidden") + config: c.object {title: "Configuration", description: "System-specific configuration properties.", additionalProperties: true, format: 'level-system-configuration'} + majorVersion: {title: "Major Version", description: "Which major version of the System is being used.", type: 'integer', minimum: 0, default: 0, format: "hidden"} + +GeneralArticleSchema = c.object { + title: "Article" + description: "Reference to a general documentation article." + required: ['original'] + format: 'latest-version-reference' + 'default': + original: null + majorVersion: 0 + links: [{rel: "db", href: "/db/article/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original Article.")#, format: "hidden") # hidden? + majorVersion: {title: "Major Version", description: "Which major version of the Article is being used.", type: 'integer', minimum: 0}#, format: "hidden"} # hidden? + +LevelSchema = c.object { + title: "Level" + description: "A spectacular level which will delight and educate its stalwart players with the sorcery of coding." + required: ["name", "description", "scripts", "thangs", "documentation"] + 'default': + name: "Ineffable Wizardry" + description: "This level is indescribably flarmy." + documentation: {specificArticles: [], generalArticles: []} + scripts: [] + thangs: [] +} +c.extendNamedProperties LevelSchema # let's have the name be the first property +_.extend LevelSchema.properties, + description: {title: "Description", description: "A short explanation of what this level is about.", type: "string", maxLength: 65536, "default": "This level is indescribably flarmy!", format: 'markdown'} + documentation: c.object {title: "Documentation", description: "Documentation articles relating to this level.", required: ["specificArticles", "generalArticles"], 'default': {specificArticles: [], generalArticles: []}}, + specificArticles: c.array {title: "Specific Articles", description: "Specific documentation articles that live only in this level.", uniqueItems: true, "default": []}, SpecificArticleSchema + generalArticles: c.array {title: "General Articles", description: "General documentation articles that can be linked from multiple levels.", uniqueItems: true, "default": []}, GeneralArticleSchema + background: c.objectId({format: 'hidden'}) + nextLevel: { + type:'object', + links: [{rel: "extra", href: "/db/level/{($)}"}, {rel:'db', href: "/db/level/{(original)}/version/{(majorVersion)}"}], + format: 'latest-version-reference', + title: "Next Level", + description: "Reference to the next level players will player after beating this one." + } + scripts: c.array {title: "Scripts", description: "An array of scripts that trigger based on what the player does and affect things outside of the core level simulation.", "default": []}, ScriptSchema + thangs: c.array {title: "Thangs", description: "An array of Thangs that make up the level.", "default": []}, LevelThangSchema + systems: c.array {title: "Systems", description: "Levels are configured by changing the Systems attached to them.", uniqueItems: true, default: []}, LevelSystemSchema # TODO: uniqueness should be based on "original", not whole thing + victory: c.object {title: "Victory Screen", default: {}, properties: {'body': {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'}, i18n: {type: "object", format: 'i18n', props: ['body'], description: "Help translate this victory message"}}} + i18n: {type: "object", format: 'i18n', props: ['name', 'description'], description: "Help translate this level"} + icon: { type: 'string', format: 'image-file', title: 'Icon' } + goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema + type: c.shortString(title: "Type", description: "What kind of level this is.", "enum": ['campaign', 'ladder', 'ladder-tutorial']) + showsGuide: c.shortString(title: "Shows Guide", description: "If the guide is shown at the beginning of the level.", "enum": ['first-time', 'always']) + +c.extendBasicProperties LevelSchema, 'level' +c.extendSearchableProperties LevelSchema +c.extendVersionedProperties LevelSchema, 'level' +c.extendPermissionsProperties LevelSchema, 'level' + +module.exports = LevelSchema + +# To test: +# 1: Copy the schema from http://localhost:3000/db/level/schema +# 2. Open up the Treema demo page http://localhost:9090/demo.html +# 3. tv4.addSchema(metaschema.id, metaschema) +# 4. S = +# 5. tv4.validateMultiple(S, metaschema) and look for errors diff --git a/app/schemas/level_session_schema.coffee b/app/schemas/level_session_schema.coffee new file mode 100644 index 000000000..4244c4771 --- /dev/null +++ b/app/schemas/level_session_schema.coffee @@ -0,0 +1,213 @@ +c = require './schemas' + +LevelSessionPlayerSchema = c.object + id: c.objectId + links: [ + { + rel: 'extra' + href: "/db/user/{($)}" + } + ] + time: + type: 'Number' + changes: + type: 'Number' + + +LevelSessionLevelSchema = c.object {required: ['original', 'majorVersion']}, + original: c.objectId({}) + majorVersion: + type: 'integer' + minimum: 0 + default: 0 + + +LevelSessionSchema = c.object + title: "Session" + description: "A single session for a given level." + + +_.extend LevelSessionSchema.properties, + # denormalization + creatorName: + type: 'string' + levelName: + type: 'string' + levelID: + type: 'string' + multiplayer: + type: 'boolean' + creator: c.objectId + links: + [ + { + rel: 'extra' + href: "/db/user/{($)}" + } + ] + created: c.date + title: 'Created' + readOnly: true + + changed: c.date + title: 'Changed' + readOnly: true + + team: c.shortString() + level: LevelSessionLevelSchema + + screenshot: + type: 'string' + + state: c.object {}, + complete: + type: 'boolean' + scripts: c.object {}, + ended: + type: 'object' + additionalProperties: + type: 'number' + currentScript: + type: [ + 'null' + 'string' + ] + currentScriptOffset: + type: 'number' + + selected: + type: [ + 'null' + 'string' + ] + playing: + type: 'boolean' + frame: + type: 'number' + thangs: + type: 'object' + additionalProperties: + title: 'Thang' + type: 'object' + properties: + methods: + type: 'object' + additionalProperties: + title: 'Thang Method' + type: 'object' + properties: + metrics: + type: 'object' + source: + type: 'string' + +# TODO: specify this more + code: + type: 'object' + + teamSpells: + type: 'object' + additionalProperties: + type: 'array' + + players: + type: 'object' + + chat: + type: 'array' + + meanStrength: + type: 'number' + + standardDeviation: + type:'number' + minimum: 0 + + totalScore: + type: 'number' + + submitted: + type: 'boolean' + + submitDate: c.date + title: 'Submitted' + + submittedCode: + type: 'object' + + isRanking: + type: 'boolean' + description: 'Whether this session is still in the first ranking chain after being submitted.' + + unsubscribed: + type: 'boolean' + description: 'Whether the player has opted out of receiving email updates about ladder rankings for this session.' + + numberOfWinsAndTies: + type: 'number' + + numberOfLosses: + type: 'number' + + scoreHistory: + type: 'array' + title: 'Score History' + description: 'A list of objects representing the score history of a session' + items: + title: 'Score History Point' + description: 'An array with the format [unix timestamp, totalScore]' + type: 'array' + items: + type: 'number' + + matches: + type: 'array' + title: 'Matches' + description: 'All of the matches a submitted session has played in its current state.' + items: + type: 'object' + properties: + date: c.date + title: 'Date computed' + description: 'The date a match was computed.' + metrics: + type: 'object' + title: 'Metrics' + description: 'Various information about the outcome of a match.' + properties: + rank: + title: 'Rank' + description: 'A 0-indexed ranking representing the player\'s standing in the outcome of a match' + type: 'number' + opponents: + type: 'array' + title: 'Opponents' + description: 'An array containing information about the opponents\' sessions in a given match.' + items: + type: 'object' + properties: + sessionID: + title: 'Opponent Session ID' + description: 'The session ID of an opponent.' + type: ['object', 'string'] + userID: + title: 'Opponent User ID' + description: 'The user ID of an opponent' + type: ['object','string'] + metrics: + type: 'object' + properties: + rank: + title: 'Opponent Rank' + description: 'The opponent\'s ranking in a given match' + type: 'number' + + + + + + +c.extendBasicProperties LevelSessionSchema, 'level.session' +c.extendPermissionsProperties LevelSessionSchema, 'level.session' + +module.exports = LevelSessionSchema diff --git a/app/schemas/level_system_schema.coffee b/app/schemas/level_system_schema.coffee new file mode 100644 index 000000000..0d7cad2c0 --- /dev/null +++ b/app/schemas/level_system_schema.coffee @@ -0,0 +1,106 @@ +c = require './schemas' +metaschema = require './metaschema' + +jitterSystemCode = """ +class Jitter extends System + constructor: (world, config) -> + super world, config + @idlers = @addRegistry (thang) -> thang.exists and thang.acts and thang.moves and thang.action is 'idle' + + update: -> + # We return a simple numeric hash that will combine to a frame hash + # help us determine whether this frame has changed in resimulations. + hash = 0 + for thang in @idlers + hash += thang.pos.x += 0.5 - Math.random() + hash += thang.pos.y += 0.5 - Math.random() + thang.hasMoved = true + return hash +""" + +PropertyDocumentationSchema = c.object { + title: "Property Documentation" + description: "Documentation entry for a property this System will add to its Thang which other Systems + might want to also use." + "default": + name: "foo" + type: "object" + description: "This System provides a 'foo' property to satisfy all one's foobar needs. Use it wisely." + required: ['name', 'type', 'description'] +}, + name: {type: 'string', pattern: c.identifierPattern, title: "Name", description: "Name of the property."} + # not actual JS types, just whatever they describe... + type: c.shortString(title: "Type", description: "Intended type of the property.") + description: {type: 'string', description: "Description of the property.", maxLength: 1000} + args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema + +DependencySchema = c.object { + title: "System Dependency" + description: "A System upon which this System depends." + "default": + #original: ? + majorVersion: 0 + required: ["original", "majorVersion"] + format: 'latest-version-reference' + links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to another System upon which this System depends.") + majorVersion: + title: "Major Version" + description: "Which major version of the System this System needs." + type: 'integer' + minimum: 0 + +LevelSystemSchema = c.object { + title: "System" + description: "A System which can affect Level behavior." + required: ["name", "description", "code", "dependencies", "propertyDocumentation", "language"] + "default": + name: "JitterSystem" + description: "This System makes all idle, movable Thangs jitter around." + code: jitterSystemCode + language: "coffeescript" + dependencies: [] # TODO: should depend on something by default + propertyDocumentation: [] +} +c.extendNamedProperties LevelSystemSchema # let's have the name be the first property +LevelSystemSchema.properties.name.pattern = c.classNamePattern +_.extend LevelSystemSchema.properties, + description: + title: "Description" + description: "A short explanation of what this System does." + type: "string" + maxLength: 2000 + "default": "This System doesn't do anything yet." + language: + type: "string" + title: "Language" + description: "Which programming language this System is written in." + "enum": ["coffeescript"] + code: + title: "Code" + description: "The code for this System, as a CoffeeScript class. TODO: add link to documentation + for how to write these." + "default": jitterSystemCode + type: "string" + format: "coffee" + js: + title: "JavaScript" + description: "The transpiled JavaScript code for this System" + type: "string" + format: "hidden" + dependencies: c.array {title: "Dependencies", description: "An array of Systems upon which this System depends.", "default": [], uniqueItems: true}, DependencySchema + propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this System will add to its Level which other Systems might want to also use.", "default": []}, PropertyDocumentationSchema + configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this System as configuration.", default: {type: 'object', additionalProperties: false}} + official: + type: "boolean" + title: "Official" + description: "Whether this is an official CodeCombat System." + "default": false + +c.extendBasicProperties LevelSystemSchema, 'level.system' +c.extendSearchableProperties LevelSystemSchema +c.extendVersionedProperties LevelSystemSchema, 'level.system' +c.extendPermissionsProperties LevelSystemSchema, 'level.system' + +module.exports = LevelSystemSchema diff --git a/app/schemas/metaschema.coffee b/app/schemas/metaschema.coffee new file mode 100644 index 000000000..4d9d7c0d8 --- /dev/null +++ b/app/schemas/metaschema.coffee @@ -0,0 +1,132 @@ +# The JSON Schema Core/Validation Meta-Schema, but with titles and descriptions added to make it easier to edit in Treema, and in CoffeeScript + +module.exports = + id: "metaschema" + displayProperty: "title" + $schema: "http://json-schema.org/draft-04/schema#" + title: "Schema" + description: "Core schema meta-schema" + definitions: + schemaArray: + type: "array" + minItems: 1 + items: { $ref: "#" } + title: "Array of Schemas" + "default": [{}] + positiveInteger: + type: "integer" + minimum: 0 + title: "Positive Integer" + positiveIntegerDefault0: + allOf: [ { $ref: "#/definitions/positiveInteger" }, { "default": 0 } ] + simpleTypes: + title: "Single Type" + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + stringArray: + type: "array" + items: { type: "string" } + minItems: 1 + uniqueItems: true + title: "String Array" + "default": [''] + type: "object" + properties: + id: + type: "string" + format: "uri" + $schema: + type: "string" + format: "uri" + "default": "http://json-schema.org/draft-04/schema#" + title: + type: "string" + description: + type: "string" + "default": {} + multipleOf: + type: "number" + minimum: 0 + exclusiveMinimum: true + maximum: + type: "number" + exclusiveMaximum: + type: "boolean" + "default": false + minimum: + type: "number" + exclusiveMinimum: + type: "boolean" + "default": false + maxLength: { $ref: "#/definitions/positiveInteger" } + minLength: { $ref: "#/definitions/positiveIntegerDefault0" } + pattern: + type: "string" + format: "regex" + additionalItems: + anyOf: [ + { type: "boolean", "default": false } + { $ref: "#" } + ] + items: + anyOf: [ + { $ref: "#" } + { $ref: "#/definitions/schemaArray" } + ] + "default": {} + maxItems: { $ref: "#/definitions/positiveInteger" } + minItems: { $ref: "#/definitions/positiveIntegerDefault0" } + uniqueItems: + type: "boolean" + "default": false + maxProperties: { $ref: "#/definitions/positiveInteger" } + minProperties: { $ref: "#/definitions/positiveIntegerDefault0" } + required: { $ref: "#/definitions/stringArray" } + additionalProperties: + anyOf: [ + { type: "boolean", "default": true } + { $ref: "#" } + ] + "default": {} + definitions: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + properties: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + patternProperties: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + dependencies: + type: "object" + additionalProperties: + anyOf: [ + { $ref: "#" } + { $ref: "#/definitions/stringArray" } + ] + "enum": + type: "array" + minItems: 1 + uniqueItems: true + "default": [''] + type: + anyOf: [ + { $ref: "#/definitions/simpleTypes" } + { + type: "array" + items: { $ref: "#/definitions/simpleTypes" } + minItems: 1 + uniqueItems: true + title: "Array of Types" + "default": ['string'] + }] + allOf: { $ref: "#/definitions/schemaArray" } + anyOf: { $ref: "#/definitions/schemaArray" } + oneOf: { $ref: "#/definitions/schemaArray" } + not: { $ref: "#" } + dependencies: + exclusiveMaximum: [ "maximum" ] + exclusiveMinimum: [ "minimum" ] + "default": {} diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee new file mode 100644 index 000000000..eebe6f954 --- /dev/null +++ b/app/schemas/schemas.coffee @@ -0,0 +1,158 @@ +#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) +me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) +# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient +me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) +me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, 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)) + + +# 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 + +# 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."} + # not actual JS types, just whatever they describe... + type: me.shortString(title: "Type", description: "Intended type of the argument.") + example: me.shortString(title: "Example", description: "Example value for the argument.") + description: {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 diff --git a/app/schemas/thang_component_schema.coffee b/app/schemas/thang_component_schema.coffee new file mode 100644 index 000000000..b6d574fdc --- /dev/null +++ b/app/schemas/thang_component_schema.coffee @@ -0,0 +1,21 @@ +c = require './schemas' + +module.exports = ThangComponentSchema = c.object { + title: "Component" + description: "Configuration for a Component that this Thang uses." + format: 'thang-component' + required: ['original', 'majorVersion'] + 'default': + majorVersion: 0 + config: {} + links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original Component being configured.", format: "hidden") + config: c.object {title: "Configuration", description: "Component-specific configuration properties.", additionalProperties: true, format: 'thang-component-configuration'} + majorVersion: + title: "Major Version" + description: "Which major version of the Component is being used." + type: 'integer' + minimum: 0 + default: 0 + format: "hidden" diff --git a/app/schemas/thang_type_schema.coffee b/app/schemas/thang_type_schema.coffee new file mode 100644 index 000000000..fb459811d --- /dev/null +++ b/app/schemas/thang_type_schema.coffee @@ -0,0 +1,153 @@ +c = require './schemas' +ThangComponentSchema = require './thang_component_schema' + +ThangTypeSchema = c.object() +c.extendNamedProperties ThangTypeSchema # name first + +ShapeObjectSchema = c.object { title: 'Shape' }, + fc: { type: 'string', title: 'Fill Color' } + lf: { type: 'array', title: 'Linear Gradient Fill' } + ls: { type: 'array', title: 'Linear Gradient Stroke' } + p: { type: 'string', title: 'Path' } + de: { type: 'array', title: 'Draw Ellipse' } + sc: { type: 'string', title: 'Stroke Color' } + ss: { type: 'array', title: 'Stroke Style' } + t: c.array {}, { type: 'number', title: 'Transform' } + m: { type: 'string', title: 'Mask' } + +ContainerObjectSchema = c.object { format: 'container' }, + b: c.array { title: 'Bounds' }, { type: 'number' } + c: c.array { title: 'Children' }, { anyOf: [ + { type: 'string', title: 'Shape Child' }, + c.object { title: 'Container Child' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number' } + ]} + +RawAnimationObjectSchema = c.object {}, + bounds: c.array { title: 'Bounds' }, { type: 'number' } + frameBounds: c.array { title: 'Frame Bounds' }, c.array { title: 'Bounds' }, { type: 'number' } + shapes: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + im : { type: 'boolean', title: 'Is Mask' } + m: { type: 'string', title: 'Uses Mask' } + containers: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number' } + o: { type: 'boolean', title: 'Starts Hidden (_off)'} + al: { type: 'number', title: 'Alpha'} + animations: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number', title: 'Transform' } + a: c.array { title: 'Arguments' } + tweens: c.array {}, + c.array { title: 'Function Chain', }, + c.object { title: 'Function Call' }, + n: { type: 'string', title: 'Name' } + a: c.array { title: 'Arguments' } + graphics: c.array {}, + bn: { type: 'string', title: 'Block Name' } + p: { type: 'string', title: 'Path' } + +PositionsSchema = c.object { title: 'Positions', description: 'Customize position offsets.' }, + registration: c.point2d { title: 'Registration Point', description: "Action-specific registration point override." } + torso: c.point2d { title: 'Torso Offset', description: "Action-specific torso offset override." } + mouth: c.point2d { title: 'Mouth Offset', description: "Action-specific mouth offset override." } + aboveHead: c.point2d { title: 'Above Head Offset', description: "Action-specific above-head offset override." } + +ActionSchema = c.object {}, + animation: { type: 'string', description: 'Raw animation being sourced', format: 'raw-animation' } + container: { type: 'string', description: 'Name of the container to show' } + relatedActions: c.object { }, + begin: { $ref: '#/definitions/action' } + end: { $ref: '#/definitions/action' } + main: { $ref: '#/definitions/action' } + fore: { $ref: '#/definitions/action' } + back: { $ref: '#/definitions/action' } + side: { $ref: '#/definitions/action' } + + "?0?011?11?11": { $ref: '#/definitions/action', title: "NW corner" } + "?0?11011?11?": { $ref: '#/definitions/action', title: "NE corner, flipped" } + "?0?111111111": { $ref: '#/definitions/action', title: "N face" } + "?11011011?0?": { $ref: '#/definitions/action', title: "SW corner, top" } + "11?11?110?0?": { $ref: '#/definitions/action', title: "SE corner, top, flipped" } + "?11011?0????": { $ref: '#/definitions/action', title: "SW corner, bottom" } + "11?110?0????": { $ref: '#/definitions/action', title: "SE corner, bottom, flipped" } + "?11011?11?11": { $ref: '#/definitions/action', title: "W face" } + "11?11011?11?": { $ref: '#/definitions/action', title: "E face, flipped" } + "011111111111": { $ref: '#/definitions/action', title: "NW elbow" } + "110111111111": { $ref: '#/definitions/action', title: "NE elbow, flipped" } + "111111111?0?": { $ref: '#/definitions/action', title: "S face, top" } + "111111?0????": { $ref: '#/definitions/action', title: "S face, bottom" } + "111111111011": { $ref: '#/definitions/action', title: "SW elbow, top" } + "111111111110": { $ref: '#/definitions/action', title: "SE elbow, top, flipped" } + "111111011?11": { $ref: '#/definitions/action', title: "SW elbow, bottom" } + "11111111011?": { $ref: '#/definitions/action', title: "SE elbow, bottom, flipped" } + "111111111111": { $ref: '#/definitions/action', title: "Middle" } + + loops: { type: 'boolean' } + speed: { type: 'number' } + goesTo: { type: 'string', description: 'Action (animation?) to which we switch after this animation.' } + frames: { type: 'string', pattern:'^[0-9,]+$', description: 'Manually way to specify frames.' } + framerate: { type: 'number', description: 'Get this from the HTML output.' } + positions: PositionsSchema + scale: { title: 'Scale', type: 'number' } + flipX: { title: "Flip X", type: 'boolean', description: "Flip this animation horizontally?" } + flipY: { title: "Flip Y", type: 'boolean', description: "Flip this animation vertically?" } + +SoundSchema = c.sound({delay: { type: 'number' }}) + +_.extend ThangTypeSchema.properties, + raw: c.object {title: 'Raw Vector Data'}, + shapes: c.object {title: 'Shapes', additionalProperties: ShapeObjectSchema} + containers: c.object {title: 'Containers', additionalProperties: ContainerObjectSchema} + animations: c.object {title: 'Animations', additionalProperties: RawAnimationObjectSchema} + kind: c.shortString { enum: ['Unit', 'Floor', 'Wall', 'Doodad', 'Misc', 'Mark'], default: 'Misc', title: 'Kind' } + + actions: c.object { title: 'Actions', additionalProperties: { $ref: '#/definitions/action' } } + soundTriggers: c.object { title: "Sound Triggers", additionalProperties: c.array({}, { $ref: '#/definitions/sound' }) }, + say: c.object { format: 'slug-props', additionalProperties: { $ref: '#/definitions/sound' } }, + defaultSimlish: c.array({}, { $ref: '#/definitions/sound' }) + swearingSimlish: c.array({}, { $ref: '#/definitions/sound' }) + rotationType: { title: 'Rotation', type: 'string', enum: ['isometric', 'fixed']} + matchWorldDimensions: { title: 'Match World Dimensions', type: 'boolean' } + shadow: { title: 'Shadow Diameter', type: 'number', format: 'meters', description: "Shadow diameter in meters" } + layerPriority: + title: 'Layer Priority' + type: 'integer' + description: "Within its layer, sprites are sorted by layer priority, then y, then z." + scale: + title: 'Scale' + type: 'number' + positions: PositionsSchema + colorGroups: c.object + title: 'Color Groups' + additionalProperties: + type:'array' + format: 'thang-color-group' + items: {type:'string'} + snap: c.object { title: "Snap", description: "In the level editor, snap positioning to these intervals.", required: ['x', 'y'] }, + x: + title: "Snap X" + type: 'number' + description: "Snap to this many meters in the x-direction." + default: 4 + y: + title: "Snap Y" + type: 'number' + description: "Snap to this many meters in the y-direction." + default: 4 + components: c.array {title: "Components", description: "Thangs are configured by changing the Components attached to them.", uniqueItems: true, format: 'thang-components-array'}, ThangComponentSchema # TODO: uniqueness should be based on "original", not whole thing + +ThangTypeSchema.definitions = + action: ActionSchema + sound: SoundSchema + +c.extendBasicProperties(ThangTypeSchema, 'thang.type') +c.extendSearchableProperties(ThangTypeSchema) +c.extendVersionedProperties(ThangTypeSchema, 'thang.type') + +module.exports = ThangTypeSchema diff --git a/app/schemas/user_schema.coffee b/app/schemas/user_schema.coffee new file mode 100644 index 000000000..a386051f3 --- /dev/null +++ b/app/schemas/user_schema.coffee @@ -0,0 +1,98 @@ +c = require './schemas' +emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] + +UserSchema = c.object {}, + name: c.shortString({title: 'Display Name', default:''}) + email: c.shortString({title: 'Email', format: 'email'}) + firstName: c.shortString({title: 'First Name'}) + lastName: c.shortString({title: 'Last Name'}) + gender: {type: 'string', 'enum': ['male', 'female']} + password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} + passwordReset: {type: 'string'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} + + facebookID: c.shortString({title: 'Facebook ID'}) + gplusID: c.shortString({title: 'G+ ID'}) + + wizardColor1: c.pct({title: 'Wizard Clothes Color'}) + volume: c.pct({title: 'Volume'}) + music: {type: 'boolean', default: true} + autocastDelay: {type: 'integer', 'default': 5000 } + lastLevel: { type: 'string' } + + emailSubscriptions: c.array {uniqueItems: true, 'default': ['announcement', 'notification']}, {'enum': emailSubscriptions} + + # server controlled + permissions: c.array {'default': []}, c.shortString() + dateCreated: c.date({title: 'Date Joined'}) + anonymous: {type: 'boolean', 'default': true} + testGroupNumber: {type: 'integer', minimum: 0, maximum: 256, exclusiveMaximum: true} + mailChimp: {type: 'object'} + hourOfCode: {type: 'boolean'} + hourOfCodeComplete: {type: 'boolean'} + + emailLower: c.shortString() + nameLower: c.shortString() + passwordHash: {type: 'string', maxLength: 256} + + # client side + emailHash: {type: 'string'} + + #Internationalization stuff + preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()} + + signedCLA: c.date({title: 'Date Signed the CLA'}) + wizard: c.object {}, + colorConfig: c.object {additionalProperties: c.colorConfig()} + + aceConfig: c.object {}, + language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript']} + keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']} + invisibles: {type: 'boolean', 'default': false} + indentGuides: {type: 'boolean', 'default': false} + behaviors: {type: 'boolean', 'default': false} + + simulatedBy: {type: 'integer', minimum: 0, default: 0} + simulatedFor: {type: 'integer', minimum: 0, default: 0} + + jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, + lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} + jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} + active: {title: 'Active', type: 'boolean', description: 'Want interview offers right now?'} + updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. The fresher, the better. Profiles go inactive after 30 days.'} + name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} + city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} + country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} + skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, + {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} + experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} + shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} + longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} + visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} + work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, + c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, + employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} + role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} + duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} + education: c.array {title: 'Education', description: 'List your academic ordeals.'}, + c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, + school: c.shortString {title: 'School', description: 'Name of your school.'} + degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} + duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} + projects: c.array {title: 'Projects', description: 'Highlight your projects to amaze employers.'}, + c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, + name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} + description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} + picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} + link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} + links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, + c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, + name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} + link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} + + jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} + jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} +c.extendBasicProperties UserSchema, 'user' + +module.exports = UserSchema diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index c75225d69..e74db5f65 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -82,8 +82,8 @@ module.exports = class SettingsView extends View buildPictureTreema: -> data = photoURL: me.get('photoURL') data.photoURL = null if data.photoURL?.search('gravatar') isnt -1 # Old style - schema = _.cloneDeep me.schema().attributes - schema.properties = _.pick me.schema().get('properties'), 'photoURL' + schema = _.cloneDeep me.schema() + schema.properties = _.pick me.schema().properties, 'photoURL' schema.required = ['photoURL'] treemaOptions = filePath: "db/user/#{me.id}" diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 1d91558f1..b7a06bd36 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -54,7 +54,7 @@ module.exports = class ArticleEditView extends View options = data: data filePath: "db/thang.type/#{@article.get('original')}" - schema: Article.schema.attributes + schema: Article.schema readOnly: true unless me.isAdmin() or @article.hasWriteAccess(me) callbacks: change: @pushChangesToPreview diff --git a/app/views/editor/components/main.coffee b/app/views/editor/components/main.coffee index 7b813595b..2c39b6086 100644 --- a/app/views/editor/components/main.coffee +++ b/app/views/editor/components/main.coffee @@ -45,7 +45,7 @@ module.exports = class ThangComponentEditView extends CocoView buildExtantComponentTreema: -> treemaOptions = supermodel: @supermodel - schema: Level.schema.get('properties').thangs.items.properties.components + schema: Level.schema.properties.thangs.items.properties.components data: _.cloneDeep @components callbacks: {select: @onSelectExtantComponent, change:@onChangeExtantComponents} noSortable: true @@ -69,7 +69,7 @@ module.exports = class ThangComponentEditView extends CocoView treemaOptions = supermodel: @supermodel - schema: { type: 'array', items: LevelComponent.schema.attributes } + schema: { type: 'array', items: LevelComponent.schema } data: ($.extend(true, {}, c) for c in components) callbacks: {select: @onSelectAddableComponent, enter: @onAddComponentEnterPressed } readOnly: true diff --git a/app/views/editor/level/component/edit.coffee b/app/views/editor/level/component/edit.coffee index 7571e9a80..3f91f1467 100644 --- a/app/views/editor/level/component/edit.coffee +++ b/app/views/editor/level/component/edit.coffee @@ -31,7 +31,7 @@ module.exports = class LevelComponentEditView extends View buildSettingsTreema: -> data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelComponent.schema.attributes + schema = _.cloneDeep LevelComponent.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings @@ -55,7 +55,7 @@ module.exports = class LevelComponentEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelComponent.schema.get('properties').configSchema + schema: LevelComponent.schema.properties.configSchema data: @levelComponent.get 'configSchema' callbacks: {change: @onConfigSchemaEdited} treemaOptions.readOnly = true unless me.isAdmin() @@ -63,7 +63,7 @@ module.exports = class LevelComponentEditView extends View @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.properties.configSchema) onConfigSchemaEdited: => @levelComponent.set 'configSchema', @configSchemaTreema.data diff --git a/app/views/editor/level/scripts_tab_view.coffee b/app/views/editor/level/scripts_tab_view.coffee index 380902d27..03855662d 100644 --- a/app/views/editor/level/scripts_tab_view.coffee +++ b/app/views/editor/level/scripts_tab_view.coffee @@ -22,7 +22,7 @@ module.exports = class ScriptsTabView extends View @dimensions = @level.dimensions() scripts = $.extend(true, [], @level.get('scripts') ? []) treemaOptions = - schema: Level.schema.get('properties').scripts + schema: Level.schema.properties.scripts data: scripts callbacks: change: @onScriptsChanged @@ -52,7 +52,7 @@ module.exports = class ScriptsTabView extends View filePath: "db/level/#{@level.get('original')}" files: @files view: @ - schema: Level.schema.get('properties').scripts.items + schema: Level.schema.properties.scripts.items data: selected.data thangIDs: thangIDs dimensions: @dimensions diff --git a/app/views/editor/level/settings_tab_view.coffee b/app/views/editor/level/settings_tab_view.coffee index 7556c4a4f..6f6822885 100644 --- a/app/views/editor/level/settings_tab_view.coffee +++ b/app/views/editor/level/settings_tab_view.coffee @@ -25,7 +25,7 @@ module.exports = class SettingsTabView extends View onLevelLoaded: (e) -> @level = e.level data = _.pick @level.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep Level.schema.attributes + schema = _.cloneDeep Level.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings thangIDs = @getThangIDs() diff --git a/app/views/editor/level/system/edit.coffee b/app/views/editor/level/system/edit.coffee index c92894cdd..338ede1e5 100644 --- a/app/views/editor/level/system/edit.coffee +++ b/app/views/editor/level/system/edit.coffee @@ -29,7 +29,7 @@ module.exports = class LevelSystemEditView extends View buildSettingsTreema: -> data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelSystem.schema.attributes + schema = _.cloneDeep LevelSystem.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings @@ -53,7 +53,7 @@ module.exports = class LevelSystemEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelSystem.schema.get('properties').configSchema + schema: LevelSystem.schema.properties.configSchema data: @levelSystem.get 'configSchema' callbacks: {change: @onConfigSchemaEdited} treemaOptions.readOnly = true unless me.isAdmin() @@ -61,7 +61,7 @@ module.exports = class LevelSystemEditView extends View @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.properties.configSchema) onConfigSchemaEdited: => @levelSystem.set 'configSchema', @configSchemaTreema.data diff --git a/app/views/editor/level/systems_tab_view.coffee b/app/views/editor/level/systems_tab_view.coffee index d04a463ff..eb4747b0e 100644 --- a/app/views/editor/level/systems_tab_view.coffee +++ b/app/views/editor/level/systems_tab_view.coffee @@ -67,7 +67,7 @@ module.exports = class SystemsTabView extends View treemaOptions = # TODO: somehow get rid of the + button, or repurpose it to open the LevelSystemAddView instead supermodel: @supermodel - schema: Level.schema.get('properties').systems + schema: Level.schema.properties.systems data: systems readOnly: true unless me.isAdmin() or @level.hasWriteAccess(me) callbacks: diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 243b1e540..8a6c4cfe5 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -140,7 +140,7 @@ module.exports = class ThangsTabView extends View return if @startsLoading data = $.extend(true, {}, @level.attributes) treemaOptions = - schema: Level.schema.get('properties').thangs + schema: Level.schema.properties.thangs data: data.thangs supermodel: @supermodel callbacks: diff --git a/app/views/editor/thang/colors_tab_view.coffee b/app/views/editor/thang/colors_tab_view.coffee index a858f4385..87c887522 100644 --- a/app/views/editor/thang/colors_tab_view.coffee +++ b/app/views/editor/thang/colors_tab_view.coffee @@ -12,7 +12,7 @@ module.exports = class ColorsTabView extends CocoView constructor: (@thangType, options) -> @listenToOnce(@thangType, 'sync', @tryToBuild) - @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) + # @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) @colorConfig = { hue: 0, saturation: 0.5, lightness: 0.5 } @spriteBuilder = new SpriteBuilder(@thangType) f = => @@ -115,7 +115,7 @@ module.exports = class ColorsTabView extends CocoView return unless @thangType.loaded and @thangType.schema().loaded data = @thangType.get('colorGroups') data ?= {} - schema = @thangType.schema().attributes.properties?.colorGroups + schema = @thangType.schema().properties?.colorGroups treemaOptions = data: data schema: schema diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 67edc6978..adc7c3a63 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -62,7 +62,7 @@ module.exports = class ThangTypeEditView extends View @thangType.fetch() @thangType.loadSchema() - @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) + # @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) @listenToOnce(@thangType, 'sync', @onThangTypeSync) @refreshAnimation = _.debounce @refreshAnimation, 500 @@ -344,7 +344,7 @@ module.exports = class ThangTypeEditView extends View buildTreema: -> data = @getThangData() - schema = _.cloneDeep ThangType.schema.attributes + schema = _.cloneDeep ThangType.schema schema.properties = _.pick schema.properties, (value, key) => not (key in ['components']) options = data: data diff --git a/app/views/modal/login_modal.coffee b/app/views/modal/login_modal.coffee index 8a433dd28..68306a03e 100644 --- a/app/views/modal/login_modal.coffee +++ b/app/views/modal/login_modal.coffee @@ -36,7 +36,7 @@ module.exports = class LoginModalView extends View loginAccount: (e) => forms.clearFormAlerts(@$el) userObject = forms.formToObject @$el - res = tv4.validateMultiple userObject, User.schema.attributes + res = tv4.validateMultiple userObject, User.schema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid @enableModalInProgress(@$el) # TODO: part of forms loginUser(userObject) diff --git a/app/views/modal/signup_modal.coffee b/app/views/modal/signup_modal.coffee index f9224cb3f..63174261f 100644 --- a/app/views/modal/signup_modal.coffee +++ b/app/views/modal/signup_modal.coffee @@ -57,7 +57,7 @@ module.exports = class SignupModalView extends View userObject.emailSubscriptions.push 'notification' unless 'notification' in userObject.emailSubscriptions else userObject.emailSubscriptions = _.without (userObject.emailSubscriptions ? []), 'announcement', 'notification' - res = tv4.validateMultiple userObject, User.schema.attributes + res = tv4.validateMultiple userObject, User.schema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid window.tracker?.trackEvent 'Finished Signup' @enableModalInProgress(@$el) diff --git a/server/routes/db.coffee b/server/routes/db.coffee index 2cbbc7df9..072b0ea8d 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -47,8 +47,8 @@ module.exports.setup = (app) -> getSchema = (req, res, moduleName) -> try - name = schemas[moduleName.replace '.', '_'] - schema = require('../' + name) + name = moduleName.replace '.', '_' + schema = require('../../app/schemas/' + name + '_schema') res.send(JSON.stringify(schema, null, '\t')) res.end()