├── .gitignore ├── wild-ideas ├── jsonary │ ├── example-schema.json │ ├── css │ │ ├── loading.gif │ │ └── page.index.css │ ├── renderers │ │ ├── images │ │ │ ├── background-fade.png │ │ │ ├── background-stripe.png │ │ │ └── background-fade-bold.png │ │ ├── api.jsonary.css │ │ ├── string-formats.js │ │ ├── list-schemas.js │ │ ├── schemas.jsonary.css │ │ ├── array-tables.js │ │ ├── api.jsonary.js │ │ ├── common.css │ │ ├── list-links.js │ │ ├── basic.jsonary.css │ │ ├── plain.jsonary.css │ │ ├── schemas.jsonary.js │ │ └── basic.jsonary.js │ ├── js │ │ └── page.index.js │ ├── LICENSE.txt │ ├── plugins │ │ ├── jsonary.render.table.css │ │ ├── jsonary.render.generate.js │ │ ├── jsonary.hash.js │ │ ├── jsonary.undo.js │ │ ├── jsonary.jstl.js │ │ └── jsonary.render.table.js │ ├── index-plain.html │ └── index.html ├── json │ ├── .htaccess │ ├── common.php │ ├── schemas.php │ ├── schemas-plain │ │ ├── idea.json │ │ └── user.json │ ├── classes │ │ ├── idea.gen.php │ │ ├── idea.php │ │ └── user.php │ ├── ideas.php │ └── users.php ├── style │ ├── grass │ │ ├── loading.gif │ │ ├── sort-asc.png │ │ ├── background.jpg │ │ ├── sort-desc.png │ │ ├── loading-small.gif │ │ └── main.css │ └── renderers │ │ └── ideas.js ├── index.html └── setup.php ├── include ├── common.php ├── match-uri-template.php ├── json-utils.php ├── json-diff.php └── jsv4.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | include/config.php 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/example-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Example schema" 3 | } 4 | -------------------------------------------------------------------------------- /wild-ideas/json/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule ^([^\.\/]+)/(.*)$ $1.php/$2 3 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/css/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/jsonary/css/loading.gif -------------------------------------------------------------------------------- /wild-ideas/style/grass/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/style/grass/loading.gif -------------------------------------------------------------------------------- /wild-ideas/style/grass/sort-asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/style/grass/sort-asc.png -------------------------------------------------------------------------------- /wild-ideas/style/grass/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/style/grass/background.jpg -------------------------------------------------------------------------------- /wild-ideas/style/grass/sort-desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/style/grass/sort-desc.png -------------------------------------------------------------------------------- /wild-ideas/style/grass/loading-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/style/grass/loading-small.gif -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/images/background-fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/jsonary/renderers/images/background-fade.png -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/images/background-stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/jsonary/renderers/images/background-stripe.png -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/images/background-fade-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geraintluff/json-store/HEAD/wild-ideas/jsonary/renderers/images/background-fade-bold.png -------------------------------------------------------------------------------- /wild-ideas/jsonary/css/page.index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F8F8F0; 3 | padding-bottom: 160px; 4 | } 5 | 6 | #url-bar { 7 | width: 80%; 8 | float: right; 9 | } 10 | 11 | #main { 12 | } 13 | 14 | .loading { 15 | background-image: url('loading.gif'); 16 | background-position: bottom; 17 | background-repeat: no-repeat; 18 | padding-bottom: 32px; 19 | } 20 | -------------------------------------------------------------------------------- /wild-ideas/json/common.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wild-ideas/json/schemas.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wild-ideas/style/renderers/ideas.js: -------------------------------------------------------------------------------- 1 | Jsonary.plugins.FancyTableRenderer({ 2 | linkHandler: function (link, submittedData, request) { 3 | return false; 4 | }, 5 | rowsPerPage: 5, 6 | sort: { 7 | '/id': true, 8 | '/title': true, 9 | '/feasibility': function (a, b) { 10 | var feasibilityOrder = ['unknown', 'impossible', 'unlikely', 'possible', 'likely', 'certain']; 11 | return feasibilityOrder.indexOf(a) - feasibilityOrder.indexOf(b); 12 | } 13 | } 14 | }) 15 | .addLinkColumn('edit', '', 'edit', 'save', true) 16 | .addLinkColumn('delete', '', 'delete', 'cancel', false) 17 | .addColumn('/id', '#') 18 | .addColumn('/title', 'Title') 19 | .addColumn('/feasibility', 'Feasibility') 20 | .register(function (data, schemas) { 21 | return schemas.containsUrl('idea#/definitions/array'); 22 | }); -------------------------------------------------------------------------------- /wild-ideas/jsonary/js/page.index.js: -------------------------------------------------------------------------------- 1 | var currentUrl = "" 2 | var interval = setInterval(function() { 3 | var hash = window.location.hash.substring(1); 4 | if (hash != currentUrl) { 5 | navigateTo(hash); 6 | } 7 | }, 100); 8 | 9 | $('#url-bar').keydown(function (e) { 10 | if (e.keyCode == 13) { 11 | $('#go').click(); 12 | } 13 | }); 14 | 15 | $('#go').click(function () { 16 | var itemUrl = $('#url-bar').val(); 17 | navigateTo(itemUrl); 18 | }); 19 | 20 | function navigateTo(itemUrl, req) { 21 | currentUrl = itemUrl; 22 | window.location = "#" + itemUrl; 23 | $('#url-bar').val(itemUrl); 24 | 25 | if (req == undefined) { 26 | req = Jsonary.getData(itemUrl); 27 | } 28 | $('#main').empty().addClass("loading"); 29 | window.scrollTo(0, 0); 30 | req.getData(function(data, request) { 31 | $('#main').removeClass("loading").empty().renderJson(data); 32 | }); 33 | } 34 | 35 | Jsonary.addLinkHandler(function(link, data, request) { 36 | navigateTo(link.href, request); 37 | return true; 38 | }); 39 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Geraint Luff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /wild-ideas/json/schemas-plain/idea.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Wild Idea", 3 | "type": "object", 4 | "properties": { 5 | "id": { 6 | "type": "integer", 7 | "readOnly": true 8 | }, 9 | "title": { 10 | "title": "Title", 11 | "type": "string" 12 | }, 13 | "feasibility": { 14 | "title": "Feasibility", 15 | "enum": [ 16 | "impossible", 17 | "unlikely", 18 | "possible", 19 | "likely", 20 | "certain", 21 | "unknown" 22 | ], 23 | "default": "unknown" 24 | } 25 | }, 26 | "required": ["id", "title", "feasibility"], 27 | "links": [ 28 | { 29 | "href": "{JSON_ROOT}/ideas/{id}", 30 | "rel": "self" 31 | }, 32 | { 33 | "href": "", 34 | "rel": "edit" 35 | }, 36 | { 37 | "href": "", 38 | "rel": "delete" 39 | } 40 | ], 41 | "definitions": { 42 | "array": { 43 | "title": "Wild Ideas List", 44 | "type": "array", 45 | "items": {"$ref": "#"}, 46 | "links": [ 47 | { 48 | "href": "", 49 | "rel": "create", 50 | "schema": { 51 | "allOf": [{"$ref": "#"}], 52 | "properties": { 53 | "id": {"enum": [0]} 54 | } 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /wild-ideas/json/classes/idea.gen.php: -------------------------------------------------------------------------------- 1 | 'ASC'); 9 | } 10 | $results = JsonStore::schemaSearch('Idea', $schema, $orderBy); 11 | foreach ($results as $idx => $result) { 12 | if ($cached = JsonStore::cached('Idea', $result->id)) { 13 | $results[$idx] = $cached; 14 | continue; 15 | } 16 | $results[$idx] = JsonStore.setCached('Idea', $result->id, new Idea($result)); 17 | } 18 | return $results; 19 | } 20 | 21 | static public function open($id) { 22 | $model = newStdClass; 23 | $model->id = $id; 24 | $results = self::search(JsonSchema::fromModel($model)); 25 | return count($results) ? $results[0] : NULL; 26 | } 27 | 28 | public function put($obj) { 29 | $obj->id = $this->id; 30 | foreach ($obj as $key => $value) { 31 | $this->$key = $value; 32 | } 33 | foreach ($this as $key => $value) { 34 | if (!isset($obj->$key)) { 35 | unset($this->$key); 36 | } 37 | } 38 | $this->save(); 39 | } 40 | 41 | public function get() { 42 | return $this; 43 | } 44 | } 45 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.render.table.css: -------------------------------------------------------------------------------- 1 | .json-array-table { 2 | border-spacing: 0; 3 | border-collapse: collapse; 4 | } 5 | 6 | .json-array-table > thead > tr > th { 7 | background-color: #EEE; 8 | border: 1px solid #888; 9 | padding: 0.3em; 10 | font-size: 0.9em; 11 | font-weight: bold; 12 | text-align: center; 13 | } 14 | 15 | .json-array-table > thead > tr > th.json-array-table-pages { 16 | border-bottom: 1px solid #BBB; 17 | background-color: #DDD; 18 | } 19 | 20 | .json-array-table > thead > tr > th.json-array-table-pages .button { 21 | font-family: Courier New, monospace; 22 | } 23 | 24 | .json-array-table > tbody > tr > td { 25 | border: 1px solid #BBB; 26 | padding: 0.2em; 27 | font-size: inherit; 28 | text-align: left; 29 | } 30 | 31 | .json-array-table > tbody > tr > td.json-array-table-full { 32 | padding: 0.3em; 33 | background-color: #EEE; 34 | } 35 | 36 | .json-array-table > tbody > tr > td.json-array-table-add { 37 | text-align: center; 38 | } 39 | 40 | .json-array-table-full-buttons { 41 | text-align: center; 42 | } 43 | 44 | .json-array-table-full-title { 45 | text-align: center; 46 | margin: -0.3em; 47 | margin-bottom: 0.5em; 48 | background-color: #CCC; 49 | border-bottom: 1px solid #BBB; 50 | font-weight: bold; 51 | padding: 0.2em; 52 | } 53 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/index-plain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSON browser 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 19 |
13 | 14 | 16 | 17 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSON browser 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 19 |
13 | 14 | 16 | 17 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/api.jsonary.css: -------------------------------------------------------------------------------- 1 | .function-definition { 2 | background-color: #F8F8F8; 3 | border: 1px solid #468; 4 | font-size: 12px; 5 | padding: 0; 6 | font-family: Trebuchet MS, sans; 7 | border-radius: 3px; 8 | } 9 | 10 | .function-definition .expand { 11 | float: right; 12 | font-weight: bold; 13 | margin-right: 1em; 14 | } 15 | 16 | .function-definition-signature { 17 | font-family: monospace; 18 | font-weight: bold; 19 | font-size: 1.1em; 20 | margin: 0; 21 | padding: 0.3em; 22 | background-color: #F0F0F8; 23 | border-bottom: 1px solid #BBB; 24 | border-radius: 3px; 25 | } 26 | 27 | .function-keyword { 28 | color: #840; 29 | } 30 | 31 | .function-name { 32 | font-style: italic; 33 | } 34 | 35 | .function-argument-name { 36 | font-family: monospace; 37 | font-weight: bold; 38 | color: #05A; 39 | } 40 | 41 | .function-definition-section { 42 | padding-left: 2em; 43 | } 44 | 45 | .function-definition-arguments { 46 | width: 100%; 47 | margin-top: 0.5em; 48 | } 49 | 50 | .function-definition-arguments .function-argument-name { 51 | text-align: right; 52 | vertical-align: top; 53 | width: 8em; 54 | } 55 | .function-definition-arguments .function-argument-name-text { 56 | padding-right: 0.5em; 57 | border-right: 1px solid black; 58 | white-space: pre; 59 | } 60 | 61 | .function-definition-section-title { 62 | padding-left: 0.3em; 63 | font-size: 1.1em; 64 | font-weight: bold; 65 | } 66 | -------------------------------------------------------------------------------- /wild-ideas/json/ideas.php: -------------------------------------------------------------------------------- 1 | save(); 21 | link_header(JSON_ROOT.'/ideas/', 'invalidates'); 22 | json_exit($idea->id); 23 | } 24 | json_error(405, "Invalid method: $method", $method); 25 | } else if ($params = matchUriTemplate('/{id}')) { 26 | $idea = Idea::open($params->id); 27 | if ($method == "GET") { 28 | json_exit($idea, SCHEMA_ROOT.'/idea'); 29 | } else if ($method == "PUT") { 30 | $idea->put($jsonData); 31 | link_header(JSON_ROOT.'/ideas/', 'invalidates'); 32 | json_exit($idea, SCHEMA_ROOT.'/idea'); 33 | } else if ($method == "DELETE") { 34 | $idea->delete(); 35 | link_header(JSON_ROOT.'/ideas/', 'invalidates'); 36 | json_exit("deleted"); 37 | } 38 | json_error(405, "Invalid method: $method", $method); 39 | } 40 | json_error(404); 41 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/string-formats.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Display string 3 | Jsonary.render.register({ 4 | renderHtml: function (data, context) { 5 | var date = new Date(data.value()); 6 | return '' + date.toLocaleString() + ''; 7 | }, 8 | filter: function (data, schemas) { 9 | return data.basicType() == "string" && data.readOnly() && schemas.formats().indexOf("date-time") != -1; 10 | } 11 | }); 12 | 13 | // Display string 14 | Jsonary.render.register({ 15 | renderHtml: function (data, context) { 16 | if (data.readOnly()) { 17 | if (context.uiState.showPassword) { 18 | return Jsonary.escapeHtml(data.value()); 19 | } else { 20 | return context.actionHtml('(show password)', 'show-password'); 21 | } 22 | } else { 23 | var inputName = context.inputNameForAction('update'); 24 | return ''; 25 | } 26 | }, 27 | action: function (context, actionName, arg1) { 28 | if (actionName == "show-password") { 29 | context.uiState.showPassword = true; 30 | return true; 31 | } else if (actionName == "update") { 32 | context.data.setValue(arg1); 33 | } 34 | }, 35 | filter: function (data, schemas) { 36 | return data.basicType() == "string" && schemas.formats().indexOf("password") != -1; 37 | } 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /include/common.php: -------------------------------------------------------------------------------- 1 | resultsPrefix = $resultsPrefix; 22 | } 23 | 24 | function query($sql) { 25 | $signature = $this->resultsPrefix.md5($sql).".json"; 26 | if (!file_exists($signature)) { 27 | $fileData = array( 28 | "query" => $sql, 29 | "results" => array() 30 | ); 31 | file_put_contents($signature, json_encode($fileData)); 32 | } 33 | $fileData = json_decode(file_get_contents($signature), TRUE); 34 | $result = array(); 35 | foreach ($fileData['results'] as $item) { 36 | foreach ($item as $key => $value) { 37 | if (!is_string($value)) { 38 | $item[$key] = json_encode($value); 39 | } 40 | } 41 | $result[] = $item; 42 | } 43 | return $result; 44 | } 45 | function escape($string) { 46 | return str_replace("'", "\'", $string); 47 | } 48 | } 49 | JsonStore::setConnection(new FakeConnection('fake/')); 50 | */ 51 | 52 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.render.generate.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | 3 | Jsonary.plugins.Generator = function (obj) { 4 | if (!obj.rendererForData) { 5 | throw "Generator must have method rendererForData"; 6 | } 7 | 8 | obj.renderHtml = function (data, context) { 9 | context.generatedRenderer = context.generatedRenderer || obj.rendererForData(data); 10 | return context.generatedRenderer.renderHtml(data, context); 11 | }; 12 | obj.enhance = function (element, data, context) { 13 | context.generatedRenderer = context.generatedRenderer || obj.rendererForData(data); 14 | if (context.generatedRenderer.enhance) { 15 | return context.generatedRenderer.enhance(element, data, context); 16 | } else if (context.generatedRenderer.render) { 17 | return context.generatedRenderer.render(element, data, context); 18 | } 19 | }; 20 | obj.action = function (context) { 21 | context.generatedRenderer = context.generatedRenderer || obj.rendererForData(context.data); 22 | return context.generatedRenderer.action.apply(context.generatedRenderer, arguments); 23 | }; 24 | obj.update = function (element, data, context) { 25 | context.generatedRenderer = context.generatedRenderer || obj.rendererForData(context.data); 26 | if (context.generatedRenderer.update) { 27 | return context.generatedRenderer.update.apply(context.generatedRenderer, arguments); 28 | } else { 29 | return this.defaultUpdate.apply(this, arguments); 30 | } 31 | }; 32 | 33 | return obj; 34 | }; 35 | 36 | })(Jsonary); -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/list-schemas.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | 3 | Jsonary.render.Components.add("LIST_SCHEMAS"); 4 | 5 | Jsonary.render.register({ 6 | component: Jsonary.render.Components.LIST_SCHEMAS, 7 | update: function (element, data, context, operation) { 8 | // We don't care about data changes - when the schemas change, a re-render is forced anyway. 9 | return false; 10 | }, 11 | renderHtml: function (data, context) { 12 | var result = ""; 13 | data.schemas().each(function (index, schema) { 14 | if (schema.title() == null) { 15 | return; 16 | } 17 | var html = '' + Jsonary.escapeHtml(schema.title()) + ''; 18 | result += context.actionHtml(html, 'view-schema', index); 19 | }); 20 | if (context.uiState.viewSchema != undefined) { 21 | var schema = data.schemas()[context.uiState.viewSchema]; 22 | result += '
'; 23 | result += context.actionHtml('
', 'hide-schema'); 24 | result += '

' + Jsonary.escapeHtml(schema.title()) + '

' + schema.referenceUrl() + '

'
25 | 					+ Jsonary.escapeHtml(JSON.stringify(schema.data.value(), null, 4))
26 | 					+ '
'; 27 | result += '
'; 28 | } 29 | result += context.renderHtml(data); 30 | return result; 31 | }, 32 | action: function (context, actionName, arg1) { 33 | if (actionName == "view-schema") { 34 | context.uiState.viewSchema = arg1; 35 | return true; 36 | } else { 37 | delete context.uiState.viewSchema; 38 | return true; 39 | } 40 | }, 41 | filter: function () { 42 | return true; 43 | } 44 | }); 45 | })(Jsonary); 46 | -------------------------------------------------------------------------------- /wild-ideas/json/classes/idea.php: -------------------------------------------------------------------------------- 1 | 'ASC'); 14 | } 15 | $sql = JsonStore::queryFromSchema('Idea', $schema, $orderBy); 16 | json_debug($sql); 17 | $results = self::mysqlQuery($sql); 18 | foreach ($results as $idx => $result) { 19 | $results[$idx] = new Idea($result); 20 | } 21 | return $results; 22 | } 23 | 24 | static public function open($id) { 25 | $schema = new StdClass; 26 | $schema->properties->id->enum = array($id); 27 | $results = self::search($schema); 28 | return count($results) ? $results[0] : NULL; 29 | } 30 | 31 | static public function create($obj) { 32 | if (!$obj) { 33 | $obj = json_decode(' 34 | { 35 | "title": "New idea" 36 | } 37 | '); 38 | } 39 | unset($obj->id); 40 | return new Idea($obj); 41 | } 42 | 43 | public function put($obj) { 44 | $obj->id = $this->id; 45 | $patch = json_diff($this, $obj); 46 | if (count($patch) == 0) { 47 | return; 48 | } 49 | foreach ($obj as $key => $value) { 50 | $this->$key = $value; 51 | } 52 | foreach ($this as $key => $value) { 53 | if (!isset($obj->$key)) { 54 | unset($this->$key); 55 | } 56 | } 57 | $this->save(); 58 | } 59 | } 60 | JsonStore::addMysqlConfig('Idea', array( 61 | "table" => "ideas", 62 | "keyColumn" => "integer/id", 63 | "columns" => array( 64 | "json" => "json", 65 | "integer/id" => "id", 66 | "string/title" => "title", 67 | "string/feasibility" => "feasibility" 68 | ) 69 | )); 70 | 71 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/schemas.jsonary.css: -------------------------------------------------------------------------------- 1 | .json-schema-obj { 2 | background-color: #F0F0E8; 3 | border: 1px solid black; 4 | font-size: 12px; 5 | padding: 0; 6 | font-family: Trebuchet MS, sans; 7 | border-radius: 3px; 8 | } 9 | 10 | .json-schema-obj .expand { 11 | float: right; 12 | font-weight: bold; 13 | margin-right: 1em; 14 | } 15 | 16 | .json-schema-obj > h1 { 17 | font-size: 1.1em; 18 | font-weight: bold; 19 | margin: 0; 20 | padding: 0.3em; 21 | background-color: #E0E0E8; 22 | border-bottom: 1px solid #BBB; 23 | border-radius: 3px; 24 | } 25 | 26 | .json-schema-obj > .content > h2, .json-schema-tab-content > h2 { 27 | font-size: 1.1em; 28 | font-weight: bold; 29 | margin: 0; 30 | padding-left: 0.3em; 31 | } 32 | 33 | .json-schema-obj > .content > .section, .json-schema-tab-content > .section { 34 | padding-left: 2em; 35 | } 36 | 37 | .json-schema-tab-bar { 38 | display: block; 39 | position: relative; 40 | padding-left: 0.5em; 41 | border-bottom: 1px solid black; 42 | } 43 | 44 | .json-schema-tab-button { 45 | position: relative; 46 | top: 1px; 47 | display: block; 48 | float: left; 49 | border: 1px solid black; 50 | border-top-left-radius: 3px; 51 | border-top-right-radius: 3px; 52 | background-color: #F2F2ED; 53 | padding: 0.3em; 54 | padding-left: 1em; 55 | padding-right: 1em; 56 | margin-right: -1px; 57 | } 58 | 59 | .json-schema-tab-button.current { 60 | top: 2px; 61 | border-bottom: none; 62 | background-color: #FFF; 63 | } 64 | 65 | .json-schema-tab-content { 66 | clear: left; 67 | padding: 0.3em; 68 | background-color: #FFF; 69 | border-radius: 3px; 70 | } 71 | 72 | .json-schema-ref { 73 | border: 1px solid black; 74 | border-radius: 3px; 75 | border-bottom: none; 76 | background-color: #F0F0E8; 77 | padding-left: 0.2em; 78 | } 79 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.hash.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | function getHash() { 3 | var index = window.location.href.indexOf('#'); 4 | return (index == -1) ? "#" : window.location.href.substring(index); 5 | } 6 | 7 | var hashJsonaryData = Jsonary.create(null); 8 | 9 | var addHistoryPoint = false; 10 | hashJsonaryData.addHistoryPoint = function () { 11 | addHistoryPoint = true; 12 | }; 13 | 14 | var ignoreUpdates = false; 15 | var lastHash = null; 16 | function updateHash() { 17 | var hashString = getHash(); 18 | if (hashString.length > 0 && hashString.charAt(0) == "#") { 19 | hashString = hashString.substring(1); 20 | } 21 | if (hashString == lastHash) { 22 | return; 23 | } 24 | lastHash = hashString; 25 | 26 | var hashData = hashString; 27 | try { 28 | hashData = Jsonary.decodeData(hashString, "application/x-www-form-urlencoded"); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | ignoreUpdate = true; 33 | hashJsonaryData.setValue(hashData); 34 | ignoreUpdate = false; 35 | } 36 | 37 | setInterval(updateHash, 100); 38 | updateHash(); 39 | 40 | var changeListeners = []; 41 | hashJsonaryData.document.registerChangeListener(function (patch) { 42 | for (var i = 0; i < changeListeners.length; i++) { 43 | changeListeners[i].call(hashJsonaryData, hashJsonaryData); 44 | } 45 | 46 | if (ignoreUpdate) { 47 | ignoreUpdate = false; 48 | return; 49 | } 50 | lastHash = Jsonary.encodeData(hashJsonaryData.value(), "application/x-www-form-urlencoded").replace("%2F", "/"); 51 | if (addHistoryPoint) { 52 | window.location.href = "#" + lastHash; 53 | } else { 54 | window.location.replace("#" + lastHash); 55 | } 56 | }); 57 | hashJsonaryData.onChange = function (callback) { 58 | changeListeners.push(callback); 59 | callback.call(hashJsonaryData, hashJsonaryData); 60 | }; 61 | 62 | Jsonary.extend({ 63 | hash: hashJsonaryData 64 | }); 65 | 66 | })(this); 67 | -------------------------------------------------------------------------------- /include/match-uri-template.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wild-ideas/json/schemas-plain/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "User", 3 | "type": "object", 4 | "properties": { 5 | "id": { 6 | "type": "integer", 7 | "readOnly": true 8 | }, 9 | "username": { 10 | "type": "string", 11 | "minLength": 1 12 | }, 13 | "name": { 14 | "type": "string", 15 | "minLength": 1 16 | }, 17 | "editUrl": {"readOnly": true}, 18 | "logoutUrl": {"readOnly": true} 19 | }, 20 | "required": ["id", "username"], 21 | "links": [ 22 | { 23 | "href": "{JSON_ROOT}/users/{id}/", 24 | "rel": "self" 25 | }, 26 | { 27 | "title": "Log in", 28 | "href": "{+loginUrl}", 29 | "rel": "login", 30 | "method": "POST", 31 | "schema": { 32 | "type": "object", 33 | "properties": { 34 | "username": { 35 | "type": "string" 36 | }, 37 | "password": { 38 | "type": "string", 39 | "format": "password" 40 | } 41 | }, 42 | "required": ["username", "password"] 43 | } 44 | }, 45 | { 46 | "title": "Log out", 47 | "href": "{+logoutUrl}", 48 | "rel": "logout", 49 | "method": "POST", 50 | "schema": { 51 | "type": "null" 52 | } 53 | }, 54 | { 55 | "href": "{+editUrl}", 56 | "rel": "edit" 57 | }, 58 | { 59 | "title": "Change password", 60 | "href": "{+editUrl}/password", 61 | "rel": "action", 62 | "method": "POST", 63 | "schema": { 64 | "type": "object", 65 | "properties": { 66 | "oldPassword": { 67 | "type": "string", 68 | "format": "password" 69 | }, 70 | "password": { 71 | "type": "string", 72 | "format": "password" 73 | } 74 | }, 75 | "required": ["oldPassword", "password"], 76 | "additionalProperties": false 77 | } 78 | } 79 | ], 80 | "definitions": { 81 | "array": { 82 | "type": "array", 83 | "items": {"$ref": "#"}, 84 | "links": [ 85 | { 86 | "href": "", 87 | "rel": "create", 88 | "schema": { 89 | "allOf": [{"$ref": "#"}], 90 | "properties": { 91 | "id": {"enum": [0]}, 92 | "password": { 93 | "type": "string", 94 | "format": "password" 95 | } 96 | }, 97 | "required": ["username", "password"] 98 | } 99 | } 100 | ] 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.undo.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var modKeyDown = false; 3 | var shiftKeyDown = false; 4 | var otherKeys = {}; 5 | 6 | // Register key down/up listeners to catch undo/redo key combos 7 | document.onkeydown = function (e) { 8 | var keyCode = (window.event != null) ? window.event.keyCode : e.keyCode; 9 | if (keyCode == 17) { 10 | modKeyDown = true; 11 | } else if (keyCode == 16) { 12 | shiftKeyDown = true; 13 | } else { 14 | otherKeys[keyCode] = true; 15 | } 16 | var otherKeyCount = 0; 17 | for (var otherKeyCode in otherKeys) { 18 | if (otherKeyCode != 90 && otherKeyCode != 89) { 19 | otherKeyCount++; 20 | } 21 | } 22 | if (otherKeyCount == 0) { 23 | if (keyCode == 90) { // Z 24 | if (modKeyDown) { 25 | if (shiftKeyDown) { 26 | Jsonary.redo(); 27 | } else { 28 | Jsonary.undo(); 29 | } 30 | } 31 | } else if (keyCode == 89) { // Y 32 | if (modKeyDown && !shiftKeyDown) { 33 | Jsonary.redo(); 34 | } 35 | } 36 | } 37 | }; 38 | document.onkeyup = function (e) { 39 | var keyCode = (window.event != null) ? window.event.keyCode : e.keyCode; 40 | if (keyCode == 17) { 41 | modKeyDown = false; 42 | } else if (keyCode == 16) { 43 | shiftKeyDown = false; 44 | } else { 45 | delete otherKeys[keyCode]; 46 | } 47 | }; 48 | 49 | var undoList = []; 50 | var redoList = []; 51 | var ignoreChanges = 0; 52 | 53 | Jsonary.registerChangeListener(function (patch, document) { 54 | if (ignoreChanges > 0) { 55 | ignoreChanges--; 56 | return; 57 | } 58 | undoList.push({patch: patch, document: document}); 59 | while (undoList.length > Jsonary.undo.historyLength) { 60 | undoList.shift(); 61 | } 62 | if (redoList.length > 0) { 63 | redoList = []; 64 | } 65 | }); 66 | 67 | Jsonary.extend({ 68 | undo: function () { 69 | var lastChange = undoList.pop(); 70 | if (lastChange != undefined) { 71 | ignoreChanges++; 72 | redoList.push(lastChange); 73 | lastChange.document.patch(lastChange.patch.inverse()); 74 | } 75 | }, 76 | redo: function () { 77 | var nextChange = redoList.pop(); 78 | if (nextChange != undefined) { 79 | ignoreChanges++; 80 | undoList.push(nextChange); 81 | nextChange.document.patch(nextChange.patch); 82 | } 83 | } 84 | }); 85 | Jsonary.undo.historyLength = 10; 86 | })(); 87 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/array-tables.js: -------------------------------------------------------------------------------- 1 | // Generic renderer for arrays 2 | // Requires "render.table" and "render.generate" plugins 3 | Jsonary.render.register(Jsonary.plugins.Generator({ 4 | rendererForData: function (data) { 5 | var renderer = new Jsonary.plugins.FancyTableRenderer(); 6 | var columnsObj = {}; 7 | function addColumnsFromSchemas(schemas, pathPrefix) { 8 | schemas = schemas.getFull(); 9 | pathPrefix = pathPrefix || ""; 10 | var basicTypes = schemas.basicTypes(); 11 | 12 | if (basicTypes.length != 1 || basicTypes[0] != "object") { 13 | var column = pathPrefix; 14 | if (!columnsObj[column]) { 15 | columnsObj[column] = true; 16 | renderer.addColumn(column, schemas.title() || column, function (data, context) { 17 | if (data.basicType() == "object") { 18 | return ''; 19 | } else { 20 | return this.defaultCellRenderHtml(data, context); 21 | } 22 | }); 23 | } 24 | } 25 | 26 | if (basicTypes.indexOf('object') != -1) { 27 | var knownProperties = schemas.knownProperties(); 28 | for (var i = 0; i < knownProperties.length; i++) { 29 | var key = knownProperties[i]; 30 | addColumnsFromSchemas(schemas.propertySchemas(key), pathPrefix + Jsonary.joinPointer([key])); 31 | } 32 | } 33 | } 34 | function addColumnsFromLink(linkDefinition, index) { 35 | var columnName = "link$" + index + "$" + linkDefinition.rel(); 36 | 37 | var linkTitle = Jsonary.escapeHtml(linkDefinition.data.get('/title') || linkDefinition.rel()); 38 | var columnTitle = ''; 39 | var linkText = linkTitle; 40 | var activeText = null, isConfirm = true; 41 | if (linkDefinition.rel() == 'edit') { 42 | activeText = 'save'; 43 | } 44 | 45 | renderer.addLinkColumn(linkDefinition, columnTitle, linkText, activeText, isConfirm); 46 | } 47 | var itemSchemas = data.schemas().indexSchemas(0).getFull(); 48 | if (data.readOnly()) { 49 | var links = itemSchemas.links(); 50 | for (var i = 0; i < links.length; i++) { 51 | var link = links[i]; 52 | addColumnsFromLink(link, i); 53 | } 54 | } 55 | addColumnsFromSchemas(itemSchemas); 56 | return renderer; 57 | }, 58 | filter: function (data, schemas) { 59 | if (data.basicType() == "array") { 60 | if (data.readOnly()) { 61 | return true; 62 | } 63 | if (!schemas.tupleTyping()) { 64 | var indexSchemas = schemas.indexSchemas(0); 65 | var itemTypes = indexSchemas.basicTypes(); 66 | if (itemTypes.length == 1 && itemTypes[0] == "object") { 67 | return true; 68 | } 69 | } 70 | } 71 | return false; 72 | } 73 | })); -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/api.jsonary.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | Jsonary.render.register({ 3 | renderHtml: function (data, context) { 4 | var result = '
'; 5 | if (!context.uiState.expanded) { 6 | result += context.actionHtml('show', 'expand'); 7 | } else { 8 | result += context.actionHtml('hide', 'collapse'); 9 | } 10 | result += '
'; 11 | result += 'function '; 12 | var title = ""; 13 | if (data.parent() != null && data.parent().basicType() == "object") { 14 | title = data.parentKey(); 15 | } 16 | result += '' + title + ''; 17 | result += '('; 18 | data.property("arguments").items(function (index, subData) { 19 | if (index > 0) { 20 | result += ', '; 21 | } 22 | var title = subData.propertyValue("title") || ("arg" + index); 23 | result += '' + title + ''; 24 | }); 25 | result += ')'; 26 | result += '
'; 27 | 28 | if (context.uiState.expanded) { 29 | result += '
'; 30 | result += context.renderHtml(data.property("description")); 31 | result += '
'; 32 | 33 | result += '

Arguments:

'; 34 | result += '
'; 35 | result += context.renderHtml(data.property("arguments")); 36 | result += '
'; 37 | 38 | result += '

Return value:

'; 39 | result += '
'; 40 | result += context.renderHtml(data.property("return")); 41 | if (data.readOnly() && !data.property("return").defined()) { 42 | result += 'undefined'; 43 | } 44 | result += '
'; 45 | } 46 | return result + '
'; 47 | }, 48 | action: function (context, actionName, tabKey) { 49 | if (actionName == "expand") { 50 | context.uiState.expanded = true; 51 | } else { 52 | context.uiState.expanded = false; 53 | } 54 | return true; 55 | }, 56 | filter: function (data, schemas) { 57 | return schemas.containsUrl('api-schema.json#/functionDefinition'); 58 | }, 59 | update: function (element, data, context, operation) { 60 | if (operation.hasPrefix(data.property("arguments")) && operation.depthFrom(data.property("arguments")) <= 2) { 61 | return true; 62 | } 63 | return this.defaultUpdate(element, data, context, operation); 64 | } 65 | }); 66 | })(Jsonary); 67 | -------------------------------------------------------------------------------- /wild-ideas/json/users.php: -------------------------------------------------------------------------------- 1 | $user) { 11 | $users[$idx] = $user->get(); 12 | } 13 | json_exit($users, SCHEMA_ROOT.'/user#/definitions/array'); 14 | } elseif ($method == "POST") { 15 | $existing = User::open($jsonData->username); 16 | if ($existing) { 17 | json_error(400, "User already exists", $jsonData->username); 18 | } 19 | $user = User::create($jsonData); 20 | $user->setPassword($user->password); 21 | $user->save(); 22 | json_exit($user->get(), SCHEMA_ROOT.'/user'); 23 | } 24 | json_error(405, "Invalid method: $method", $method); 25 | } else if ($params = matchUriTemplate('/login')) { 26 | if ($method == "POST") { 27 | $user = User::openUsername($jsonData->username); 28 | if ($user && $user->checkPassword($jsonData->password)) { 29 | $user->login(); 30 | link_header(JSON_ROOT.'/', 'invalidates'); 31 | json_exit(TRUE); 32 | } 33 | json_error(401, "Incorrect username/password"); 34 | } 35 | json_error(405, "Invalid method: $method", $method); 36 | } else if ($params = matchUriTemplate('/logout')) { 37 | if ($method == "POST") { 38 | User::logout(); 39 | link_header(JSON_ROOT.'/', 'invalidates'); 40 | json_exit(TRUE); 41 | } 42 | json_error(405, "Invalid method: $method", $method); 43 | } else if ($params = matchUriTemplate('/{userId}/')) { 44 | $user = ($params->userId == "me") ? User::current($params->userId) : User::open($params->userId); 45 | if (!$user) { 46 | if ($params->userId == "me") { 47 | $user = User::anonymous(); 48 | json_exit($user->get(), SCHEMA_ROOT."/user"); 49 | } else { 50 | json_error(404, "User not found", $params->userId); 51 | } 52 | } 53 | if ($method == "GET") { 54 | json_exit($user->get(), SCHEMA_ROOT.'/user'); 55 | } else if ($method == "PUT") { 56 | $user->put($jsonData); 57 | $user->save(); 58 | json_exit($user->get(), SCHEMA_ROOT.'/user'); 59 | } 60 | json_error(405, "Invalid method: $method", $method); 61 | } else if ($params = matchUriTemplate('/{username}/password')) { 62 | $user = User::open($params->username); 63 | if ($method == "PUT" || $method == "POST") { 64 | if (!$user->checkPassword($jsonData->oldPassword)) { 65 | json_error(403, "Incorrect password"); 66 | } 67 | $user->setPassword($jsonData->password); 68 | $user->save(); 69 | json_exit($user->get(), SCHEMA_ROOT.'/user'); 70 | } 71 | json_error(405, "Invalid method: $method", $method); 72 | } 73 | json_error(404); 74 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/common.css: -------------------------------------------------------------------------------- 1 | .link { 2 | color: #05C; 3 | } 4 | 5 | .link:hover { 6 | color: #07F; 7 | text-decoration: underline; 8 | } 9 | 10 | /****** Prompt *******/ 11 | 12 | .prompt-outer { 13 | display: inline; 14 | position: relative; 15 | text-align: center; 16 | position: absolute; 17 | left: 10%; 18 | width: 80%; 19 | z-index: 1000; 20 | } 21 | 22 | .prompt-inner { 23 | display: inline-block; 24 | } 25 | 26 | .prompt-overlay { 27 | position: fixed; 28 | top: 0; 29 | left: 0; 30 | width: 70%; 31 | height: 100%; 32 | background-color: #000; 33 | padding-left: 15%; 34 | padding-right: 15%; 35 | background-color: rgba(100, 100, 100, 0.3); 36 | background-image: URL('images/background-stripe.png'); 37 | } 38 | 39 | .prompt-box { 40 | position: relative; 41 | text-align: left; 42 | background-color: white; 43 | border: 2px solid black; 44 | border-radius: 10px; 45 | padding: 1em; 46 | padding-bottom: 1.5em; 47 | padding-top: 0.5em; 48 | } 49 | 50 | .prompt-box h1 { 51 | text-align: center; 52 | font-size: 1.3em; 53 | font-weight: bold; 54 | border: none; 55 | border-bottom: 1px solid black; 56 | margin: 0; 57 | } 58 | 59 | .prompt-box h2 { 60 | color: #666; 61 | text-align: center; 62 | font-size: 1.1em; 63 | font-style: italic; 64 | border: none; 65 | margin: 0; 66 | padding: 0; 67 | margin-bottom: 1em; 68 | } 69 | 70 | .prompt-buttons { 71 | position: relative; 72 | top: -13px; 73 | border: 2px solid black; 74 | border-bottom-left-radius: 10px; 75 | border-bottom-right-radius: 10px; 76 | padding-top: 0.3em; 77 | padding-bottom: 0.3em; 78 | } 79 | 80 | /************ Dialog *************/ 81 | 82 | .dialog-anchor { 83 | position: relative; 84 | height: 1em; 85 | } 86 | 87 | .dialog-overlay { 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | bottom: 0; 92 | right: 0; 93 | background-color: rgba(100, 100, 100, 0.5); 94 | background-image: URL('images/background-stripe.png'); 95 | opacity: 0.3; 96 | } 97 | 98 | .dialog-box { 99 | position: absolute; 100 | top: -0.1em; 101 | left: -0em; 102 | width: auto; 103 | height: auto; 104 | padding: 0.3em; 105 | padding-top: 1.3em; 106 | border: 2px solid black; 107 | border-radius: 5px; 108 | background-color: #FFF; 109 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1), 0px 3px 5px rgba(0, 0, 0, 0.3); 110 | min-width: 100px; 111 | z-index: 1000; 112 | } 113 | 114 | .dialog-title { 115 | display: block; 116 | margin: -0.3em; 117 | margin-top: -1.3em; 118 | margin-bottom: 0.3em; 119 | padding-left: 3em; 120 | padding-right: 3em; 121 | background-color: #EEE; 122 | border-bottom: 2px solid black; 123 | font-weight: bold; 124 | border-top-left-radius: 4px; 125 | border-top-right-radius: 4px; 126 | white-space: pre; 127 | } 128 | 129 | .dialog-close { 130 | position: absolute; 131 | display: block; 132 | top: 0; 133 | left: 0.2em; 134 | font-weight: normal; 135 | font-size: 0.8em; 136 | font-family: Verdana; 137 | } 138 | 139 | /************ Buttons *************/ 140 | 141 | .button { 142 | display: inline; 143 | color: #444; 144 | font-weight: bold; 145 | text-decoration: none; 146 | margin-left: 0.5em; 147 | margin-right: 0.5em; 148 | padding-left: 0.5em; 149 | padding-right: 0.5em; 150 | border: 1px solid #888; 151 | border-radius: 3px; 152 | background-color: #DDD; 153 | background-image: URL('images/background-fade.png'); 154 | background-repeat: repeat-x; 155 | background-position: top; 156 | } 157 | 158 | .button:hover { 159 | color: #222; 160 | background-image: URL('images/background-fade-bold.png'); 161 | } 162 | 163 | .button.link { 164 | color: #05C; 165 | background-color: #E0E8F0; 166 | background-image: none; 167 | } 168 | 169 | .button.link:hover { 170 | color: #07F; 171 | background-color: #E8F0F8; 172 | background-image: none; 173 | } 174 | 175 | .button.action { 176 | color: #000; 177 | background-color: #F0B870; 178 | background-image: none; 179 | font-weight: bold; 180 | border-width: 2px; 181 | } 182 | 183 | .button.action:hover { 184 | background-color: #FCC47C; 185 | } 186 | 187 | .button.disabled, .button.disabled:hover { 188 | border-color: #CCC; 189 | color: #888; 190 | background-color: #EEE; 191 | background-image: none; 192 | } -------------------------------------------------------------------------------- /wild-ideas/json/classes/user.php: -------------------------------------------------------------------------------- 1 | 'ASC'); 14 | } 15 | $sql = JsonStore::queryFromSchema('User', $schema, $orderBy); 16 | json_debug($sql); 17 | $results = self::mysqlQuery($sql); 18 | foreach ($results as $idx => $result) { 19 | $results[$idx] = new User($result); 20 | } 21 | return $results; 22 | } 23 | 24 | static public function open($userId) { 25 | if (JsonStore::cached('User', $userId)) { 26 | return JsonStore::cached('User', $userId); 27 | } 28 | $schema = new StdClass; 29 | $schema->properties->id->enum = array($userId); 30 | $results = self::search($schema); 31 | if (count($results) == 1) { 32 | return JsonStore::setCached('User', $userId, $results[0]); 33 | } 34 | return NULL; 35 | } 36 | 37 | static public function openUsername($username) { 38 | $schema = new StdClass; 39 | $schema->properties->username->enum = array((string)$username); 40 | $results = self::search($schema); 41 | if (count($results) == 1) { 42 | return $results[0]; 43 | return JsonStore::setCached('User', $user->id, $results[0]); 44 | } 45 | return NULL; 46 | } 47 | 48 | static public function create($obj) { 49 | if (!$obj) { 50 | $obj = json_decode(' 51 | { 52 | "username": "user'.rand().'" 53 | } 54 | '); 55 | } 56 | unset($obj->id); 57 | return new User($obj); 58 | } 59 | 60 | static public function current() { 61 | if (!isset($_SESSION['current_user'])) { 62 | return NULL; 63 | } 64 | $userId = $_SESSION['current_user']; 65 | return self::open($userId); 66 | } 67 | 68 | static public function anonymous() { 69 | if ($cached = JsonStore::cached('User', 'anonymous')) { 70 | return $cached; 71 | } 72 | return JsonStore::setCached('User', 'anonymous', self::create((object)array( 73 | "name" => "anonymous", 74 | "username" => $_SERVER['REMOTE_ADDR'] 75 | ))); 76 | } 77 | 78 | static public function logout() { 79 | link_header(JSON_ROOT.'/', 'invalidates'); 80 | link_header(JSON_ROOT.'/users/me/', 'invalidates'); 81 | unset($_SESSION['current_user']); 82 | } 83 | 84 | public function login() { 85 | link_header(JSON_ROOT.'/', 'invalidates'); 86 | link_header(JSON_ROOT.'/users/me/', 'invalidates'); 87 | link_header(JSON_ROOT."/users/{$this->id}/", 'invalidates'); 88 | $_SESSION['current_user'] = $this->id; 89 | } 90 | 91 | public function get() { 92 | $clone = clone($this); 93 | unset($clone->password); 94 | if (!isset($this->id)) { 95 | $clone->loginUrl = JSON_ROOT.'/users/login'; 96 | } else if ($this == User::current()) { 97 | $clone->editUrl = ""; 98 | $clone->logoutUrl = JSON_ROOT.'/users/logout'; 99 | } 100 | return $clone; 101 | } 102 | 103 | public function put($obj) { 104 | $obj->id = $this->id; 105 | $obj->password = $this->password; 106 | foreach ($obj as $key => $value) { 107 | $this->$key = $value; 108 | } 109 | foreach ($this as $key => $value) { 110 | if (!isset($obj->$key)) { 111 | unset($this->$key); 112 | } 113 | } 114 | } 115 | 116 | public function checkPassword($password) { 117 | $hash = hash($this->password->algorithm, $this->password->salt.$password); 118 | return $hash == $this->password->hash; 119 | } 120 | 121 | public function setPassword($password) { 122 | $this->password = new StdClass; 123 | $this->password->salt = openssl_random_pseudo_bytes(20); 124 | $this->password->algorithm = "sha256"; // yeah, I know - but bcrypt is only built-in for PHP 5.5+ 125 | $this->password->hash = hash($this->password->algorithm, $this->password->salt.$password); 126 | } 127 | } 128 | JsonStore::addMysqlConfig('User', array( 129 | "table" => "users", 130 | "keyColumn" => "integer/id", 131 | "columns" => array( 132 | "integer/id" => "id", 133 | "string/username" => "username", 134 | "string/name" => "name", 135 | "string/password/salt" => "pw_salt", 136 | "string/password/algorithm" => "pw_algo", 137 | "string/password/hash" => "pw_hash" 138 | ) 139 | )); 140 | 141 | ?> -------------------------------------------------------------------------------- /include/json-utils.php: -------------------------------------------------------------------------------- 1 | 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Moved Temporarily', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported'); 14 | if (isset($statusTexts[$code])) { 15 | return $statusTexts[$code]; 16 | } else { 17 | return "Unknown Code"; 18 | } 19 | } 20 | 21 | if (!function_exists('http_response_code')) { 22 | function http_response_code($code = NULL) { 23 | if ($code !== NULL) { 24 | $text = http_response_text($code); 25 | $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'); 26 | header($protocol . ' ' . $code . ' ' . $text); 27 | $GLOBALS['http_response_code'] = $code; 28 | } else { 29 | $code = (isset($GLOBALS['http_response_code']) ? $GLOBALS['http_response_code'] : 200); 30 | } 31 | return $code; 32 | } 33 | } 34 | 35 | $jsonLogMessages = array(); 36 | function json_debug($message) { 37 | global $jsonLogMessages; 38 | if (strlen($message) > 200) { 39 | $message = substr($message, 0, 197)."..."; 40 | } 41 | if (DEBUG) { 42 | $jsonLogMessages[] = $message; 43 | $message = str_replace("\n", " / ", $message); 44 | header("X-Debug: $message", false); 45 | } 46 | } 47 | 48 | function json_error($message, $statusCode=NULL, $data=NULL) { 49 | global $jsonLogMessages; 50 | if (is_integer($message)) { 51 | $tmp = $statusCode; 52 | $statusCode = $message; 53 | $message = $tmp; 54 | } 55 | $statusCode = is_null($statusCode) ? 400 : $statusCode; 56 | http_response_code($statusCode); 57 | $result = (object)array( 58 | "statusCode" => $statusCode, 59 | "statusText" => http_response_text($statusCode), 60 | "message" => $message, 61 | "data" => $data 62 | ); 63 | if (DEBUG) { 64 | $result->debug = TRUE; 65 | $result->debugMessages = $jsonLogMessages; 66 | $backtrace = array_slice(debug_backtrace(), 1); 67 | $result->trace = $backtrace; 68 | } 69 | header("Content-Type: application/json"); 70 | echo json_encode($result); 71 | exit(1); 72 | } 73 | 74 | function json_exit($data, $schemaUrl=NULL) { 75 | return json_exit_raw(json_encode($data), $schemaUrl); 76 | } 77 | 78 | function json_exit_raw($jsonText, $schemaUrl=NULL) { 79 | global $jsonLogMessages; 80 | if ($schemaUrl != NULL) { 81 | header("Content-Type: application/json; profile=".$schemaUrl); 82 | } else { 83 | header("Content-Type: application/json"); 84 | } 85 | echo $jsonText; 86 | exit(0); 87 | } 88 | 89 | function json_handle_error($errorNumber, $errorString, $errorFile, $errorLine) { 90 | if ($errorNumber&E_NOTICE) { 91 | if (DEBUG) { 92 | json_debug("Notice $errorNumber: $errorString in $errorFile:$errorLine"); 93 | } 94 | } elseif ($errorNumber&E_STRICT) { 95 | if (DEBUG) { 96 | json_debug("Strict warning $errorNumber: $errorString in $errorFile:$errorLine"); 97 | } 98 | } elseif ($errorNumber&E_WARNING) { 99 | if (DEBUG) { 100 | json_debug("Warning $errorNumber: $errorString in $errorFile:$errorLine"); 101 | } 102 | } else { 103 | json_error("Error $errorNumber: $errorString", 500, array("file" => $errorFile, "line" => $errorLine)); 104 | } 105 | } 106 | 107 | function link_header($url, $rel, $params=NULL) { 108 | if (is_array($rel)) { 109 | $params = $rel; 110 | } else if (!$params) { 111 | $params = array("rel" => $rel); 112 | } else { 113 | $params['rel'] = $rel; 114 | } 115 | $parts = array("Link: <$url>"); 116 | foreach ($params as $key => $value) { 117 | if (strpos($value, ' ') !== FALSE) { 118 | $value = json_encode($value); 119 | } 120 | $parts[] = "$key=$value"; 121 | } 122 | header(implode("; ", $parts), FALSE); 123 | } 124 | 125 | set_error_handler('json_handle_error', E_ALL|E_STRICT); 126 | 127 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/list-links.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | 3 | Jsonary.render.Components.add("LIST_LINKS"); 4 | 5 | Jsonary.render.register({ 6 | component: Jsonary.render.Components.LIST_LINKS, 7 | update: function (element, data, context, operation) { 8 | // We don't care about data changes - when the links change, a re-render is forced anyway. 9 | return false; 10 | }, 11 | renderHtml: function (data, context) { 12 | if (!data.readOnly()) { 13 | return context.renderHtml(data); 14 | } 15 | var result = ""; 16 | if (context.uiState.editInPlace) { 17 | var html = 'save'; 18 | result += context.actionHtml(html, "submit"); 19 | var html = 'cancel'; 20 | result += context.actionHtml(html, "cancel"); 21 | result += context.renderHtml(context.uiState.submissionData, '~linkData'); 22 | return result; 23 | } 24 | 25 | var links = data.links(); 26 | if (links.length) { 27 | result += ''; 28 | for (var i = 0; i < links.length; i++) { 29 | var link = links[i]; 30 | var html = '' + Jsonary.escapeHtml(link.title || link.rel) + ''; 31 | result += context.actionHtml(html, 'follow-link', i); 32 | } 33 | result += ''; 34 | } 35 | 36 | if (context.uiState.submitLink != undefined) { 37 | var link = data.links()[context.uiState.submitLink]; 38 | result += '
'; 39 | result += context.actionHtml('
', 'cancel'); 40 | result += '

' + Jsonary.escapeHtml(link.title || link.rel) + '

' + Jsonary.escapeHtml(link.method) + " " + Jsonary.escapeHtml(link.href) + '

'; 41 | result += '
' + context.renderHtml(context.uiState.submissionData, '~linkData') + '
'; 42 | result += '
'; 43 | result += '
'; 44 | result += context.actionHtml('Submit', 'submit'); 45 | result += context.actionHtml('cancel', 'cancel'); 46 | result += '
'; 47 | result += '
'; 48 | } 49 | 50 | result += context.renderHtml(data, "data"); 51 | return result; 52 | }, 53 | action: function (context, actionName, arg1) { 54 | if (actionName == "follow-link") { 55 | var link = context.data.links()[arg1]; 56 | if (link.method == "GET" && link.submissionSchemas.length == 0) { 57 | // There's no data to prompt for, and GET links are safe, so we don't put up a dialog 58 | link.follow(); 59 | return false; 60 | } 61 | context.uiState.submitLink = arg1; 62 | if (link.method == "PUT" && link.submissionSchemas.length == 0) { 63 | context.uiState.editing = context.data.editableCopy(); 64 | context.uiState.submissionData = context.data.editableCopy(); 65 | } else { 66 | context.uiState.submissionData = Jsonary.create().addSchema(link.submissionSchemas); 67 | link.submissionSchemas.createValue(function (submissionValue) { 68 | context.uiState.submissionData.setValue(submissionValue); 69 | }); 70 | } 71 | if (link.method == "PUT") { 72 | context.uiState.editInPlace = true; 73 | } 74 | return true; 75 | } else if (actionName == "submit") { 76 | var link = context.data.links()[context.uiState.submitLink]; 77 | link.follow(context.uiState.submissionData); 78 | delete context.uiState.submitLink; 79 | delete context.uiState.editInPlace; 80 | delete context.uiState.submissionData; 81 | return true; 82 | } else { 83 | delete context.uiState.submitLink; 84 | delete context.uiState.editInPlace; 85 | delete context.uiState.submissionData; 86 | return true; 87 | } 88 | }, 89 | filter: function () { 90 | return true; 91 | }, 92 | saveState: function (uiState, subStates) { 93 | var result = {}; 94 | for (var key in subStates.data) { 95 | result[key] = subStates.data[key]; 96 | } 97 | if (result.link != undefined || result.inPlace != undefined || result.linkData != undefined || result[""] != undefined) { 98 | var newResult = {"":"-"}; 99 | for (var key in result) { 100 | newResult["-" + key] = result[key]; 101 | } 102 | result = newResult; 103 | } 104 | if (uiState.submitLink !== undefined) { 105 | var parts = [uiState.submitLink]; 106 | parts.push(uiState.editInPlace ? 1 : 0); 107 | parts.push(this.saveStateData(uiState.submissionData)); 108 | result['link'] = parts.join("-"); 109 | } 110 | return result; 111 | }, 112 | loadState: function (savedState) { 113 | var uiState = {}; 114 | if (savedState['link'] != undefined) { 115 | var parts = savedState['link'].split("-"); 116 | uiState.submitLink = parseInt(parts.shift()) || 0; 117 | if (parseInt(parts.shift())) { 118 | uiState.editInPlace = true 119 | } 120 | uiState.submissionData = this.loadStateData(parts.join("-")); 121 | delete savedState['link']; 122 | if (!uiState.submissionData) { 123 | uiState = {}; 124 | } 125 | } 126 | if (savedState[""] != undefined) { 127 | delete savedState[""]; 128 | var newSavedState = {}; 129 | for (var key in savedState) { 130 | newSavedState[key.substring(1)] = savedState[key]; 131 | } 132 | savedState = newSavedState; 133 | } 134 | return [ 135 | uiState, 136 | {data: savedState} 137 | ]; 138 | } 139 | }); 140 | 141 | })(Jsonary); 142 | -------------------------------------------------------------------------------- /include/json-diff.php: -------------------------------------------------------------------------------- 1 | "replace", 10 | "path" => "", 11 | "value" => $b, 12 | "oldValue" => $a 13 | ); 14 | return $patch; 15 | } 16 | $totalKeys = 0; 17 | $addedKeys = 0; 18 | $deletedKeys = 0; 19 | $changedKeys = 0; 20 | $subChanges = array(); 21 | foreach ($b as $key => $value) { 22 | $totalKeys++; 23 | $path = "/".str_replace("/", "~1", str_replace("~", "~0", $key)); 24 | if (!property_exists($a, $key)) { 25 | $addedKeys++; 26 | $subChanges[] = (object)array( 27 | "op" => "add", 28 | "path" => $path, 29 | "value" => $value 30 | ); 31 | } else { 32 | $subPatch = json_diff_inner($a->$key, $value); 33 | if (count($subPatch)) { 34 | $changedKeys++; 35 | } 36 | foreach ($subPatch as $change) { 37 | $change->path = $path.$change->path; 38 | $subChanges[] = $change; 39 | } 40 | } 41 | } 42 | foreach ($a as $key => $value) { 43 | $totalKeys++; 44 | $path = "/".str_replace("/", "~1", str_replace("~", "~0", $key)); 45 | if (!property_exists($b, $key)) { 46 | $deletedKeys++; 47 | $subChanges[] = (object)array( 48 | "op" => "remove", 49 | "path" => $path, 50 | "oldValue" => $value 51 | ); 52 | } 53 | } 54 | if ($addedKeys + $deletedKeys + $changedKeys > $totalKeys*0.5) { 55 | $patch[] = (object)array( 56 | "op" => "replace", 57 | "path" => "", 58 | "value" => $b, 59 | "oldValue" => $a 60 | ); 61 | } else { 62 | $patch = array_merge($patch, $subChanges); 63 | } 64 | } elseif (is_array($a)) { 65 | if (!is_array($b)) { 66 | $patch[] = (object)array( 67 | "op" => "replace", 68 | "path" => "", 69 | "value" => $b, 70 | "oldValue" => $a 71 | ); 72 | return $patch; 73 | } 74 | $arrayChanges = array(); 75 | $indexA = 0; 76 | $copyA = $a; 77 | while (TRUE) { 78 | $matchA = $matchB = NULL; 79 | $distanceA = $distanceB = 0; 80 | while ((isset($distanceA) || isset($distanceB)) && !isset($matchA)) { 81 | if (isset($distanceA)) { 82 | if ($indexA + $distanceA < count($copyA)) { 83 | $matchIndex = $indexA; 84 | while ($matchIndex <= $indexA + $distanceA) { 85 | if ($b[$matchIndex] == $copyA[$indexA + $distanceA]) { 86 | $matchA = $indexA + $distanceA; 87 | $matchB = $matchIndex; 88 | break; 89 | } 90 | $matchIndex++; 91 | } 92 | $distanceA++; 93 | } else { 94 | $distanceA = NULL; 95 | } 96 | } 97 | if (isset($distanceB)) { 98 | if ($indexA + $distanceB < count($b)) { 99 | $matchIndex = $indexA; 100 | while ($matchIndex <= $indexA + $distanceB) { 101 | if ($copyA[$matchIndex] == $b[$indexA + $distanceB]) { 102 | $matchA = $matchIndex; 103 | $matchB = $indexA + $distanceB; 104 | break; 105 | } 106 | $matchIndex++; 107 | } 108 | $distanceB++; 109 | } else { 110 | $distanceB = NULL; 111 | } 112 | } 113 | } 114 | if (!isset($matchA)) { 115 | $matchA = count($copyA); 116 | $matchB = count($b); 117 | } 118 | for ($index = $indexA; $index < $matchA && $index < $matchB; $index++) { 119 | $arrayChanges[] = (object)array( 120 | "op" => "replace", 121 | "path" => "/$index", 122 | "value" => $b[$index], 123 | "oldValue" => $a[$index] 124 | ); 125 | } 126 | for ($index = $matchA; $index < $matchB; $index++) { 127 | $arrayChanges[] = (object)array( 128 | "op" => "add", 129 | "path" => "/$index", 130 | "value" => $b[$index] 131 | ); 132 | } 133 | for ($index = $matchB; $index < $matchA; $index++) { 134 | $arrayChanges[] = (object)array( 135 | "op" => "remove", 136 | "path" => "/$index", 137 | "oldValue" => $copyA[$index] 138 | ); 139 | } 140 | if ($matchB > 0) { 141 | $copyA = array_merge(array_fill(0, $matchB, NULL), array_slice($copyA, $matchA)); 142 | } else { 143 | $copyA = array_slice($copyA, $matchA); 144 | } 145 | $indexA = $matchB + 1; 146 | if ($matchA >= count($copyA) && $matchB >= count($b)) { 147 | break; 148 | } 149 | } 150 | if (count($arrayChanges) > (count($a) + count($b))/4) { 151 | $patch[] = (object)array( 152 | "op" => "replace", 153 | "path" => "", 154 | "value" => $b, 155 | "oldValue" => $a 156 | ); 157 | } else { 158 | foreach ($arrayChanges as $change) { 159 | if ($change->op == "replace") { 160 | $subPatch = json_diff_inner($change->oldValue, $change->value); 161 | foreach ($subPatch as $subChange) { 162 | $subChange->path = $change->path.$subChange->path; 163 | $patch[] = $subChange; 164 | } 165 | } else { 166 | $patch[] = $change; 167 | } 168 | } 169 | } 170 | } else { 171 | if ($a != $b) { 172 | $patch[] = (object)array( 173 | "op" => "replace", 174 | "path" => "", 175 | "value" => $b, 176 | "oldValue" => $a 177 | ); 178 | } 179 | } 180 | return $patch; 181 | } 182 | 183 | function json_diff($a, $b) { 184 | $patch = json_diff_inner($a, $b); 185 | // TODO: cleanup, matching moves from replace/add/removes 186 | return $patch; 187 | } 188 | 189 | /* 190 | $testA = (object)array( 191 | "arr" => array("Yo", "Hello"), 192 | "test" => "here", 193 | "same" => "same", 194 | "same2" => "same" 195 | ); 196 | $testB = (object)array( 197 | "arr" => array("Yo", "Hello", "Hi"), 198 | "test2" => "foo", 199 | "same" => "same", 200 | "same2" => "same" 201 | ); 202 | header("Content-Type: application/json"); 203 | die(json_encode(json_diff($testA, $testB))); 204 | */ 205 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.jstl.js: -------------------------------------------------------------------------------- 1 | (function (publicApi) { 2 | var templateMap = {}; 3 | var loadedUrls = {}; 4 | function loadTemplates(url) { 5 | if (url == undefined) { 6 | if (typeof document == "undefined") { 7 | return; 8 | } 9 | var scripts = document.getElementsByTagName("script"); 10 | var lastScript = scripts[scripts.length - 1]; 11 | url = lastScript.getAttribute("src"); 12 | } 13 | if (loadedUrls[url]) { 14 | return; 15 | } 16 | loadedUrls[url] = true; 17 | 18 | var code = ""; 19 | if (typeof XMLHttpRequest != 'undefined') { 20 | // In browser 21 | var xhr = new XMLHttpRequest(); 22 | xhr.open("GET", url, false); 23 | xhr.send(); 24 | code = xhr.responseText; 25 | } else if (typeof require != 'undefined') { 26 | // Server-side 27 | var fs = require('fs'); 28 | code = fs.readFileSync(url).toString(); 29 | } 30 | 31 | var parts = (" " + code).split(/\/\*\s*[Tt]emplate:/); 32 | parts.shift(); 33 | for (var i = 0; i < parts.length; i++) { 34 | var part = parts[i]; 35 | part = part.substring(0, part.indexOf("*/")); 36 | var endOfLine = part.indexOf("\n"); 37 | var key = part.substring(0, endOfLine).trim(); 38 | var template = part.substring(endOfLine + 1); 39 | templateMap[key] = template; 40 | } 41 | } 42 | function getTemplate(key) { 43 | loadTemplates(); 44 | var rawCode = templateMap[key]; 45 | if (rawCode) { 46 | return create(rawCode); 47 | } 48 | return null; 49 | } 50 | function create(rawCode) { 51 | return { 52 | toString: function () {return this.code;}, 53 | code: rawCode, 54 | compile: function (directEvalFunction, constFunctions, additionalParams) { 55 | return compile(this.code, directEvalFunction, constFunctions, additionalParams); 56 | } 57 | }; 58 | } 59 | 60 | function compile(template, directEvalFunction, headerText, additionalParams) { 61 | if (directEvalFunction == undefined) { 62 | directEvalFunction = publicApi.defaultFunction; 63 | } 64 | if (headerText == undefined) { 65 | headerText = publicApi.defaultHeaderCode; 66 | } 67 | if (additionalParams == undefined) { 68 | additionalParams = {}; 69 | } 70 | var constants = []; 71 | var variables = []; 72 | 73 | var substitutionFunctionName = "subFunc" + Math.floor(Math.random()*1000000000); 74 | var jscode = '(function () {\n'; 75 | 76 | var directFunctions = []; 77 | var directFunctionVarNames = []; 78 | for (var key in additionalParams) { 79 | if (additionalParams[key]) { 80 | directFunctionVarNames.push(key); 81 | directFunctions.push(additionalParams[key]); 82 | } 83 | } 84 | var parts = (" " + template).split(/<\?js|<\?|<%/g); 85 | var initialString = parts.shift().substring(1); 86 | if (headerText) { 87 | jscode += "\n" + headerText + "\n"; 88 | } 89 | jscode += ' var _arguments = arguments;\n'; 90 | if (additionalParams['echo'] !== undefined) { 91 | jscode += ' echo(' + JSON.stringify(initialString) + ');\n'; 92 | } else { 93 | var resultVariableName = "result" + Math.floor(Math.random()*1000000000); 94 | jscode += ' var ' + resultVariableName + ' = ' + JSON.stringify(initialString) + ';\n'; 95 | jscode += ' var echo = function (str) {' + resultVariableName + ' += str;};\n'; 96 | } 97 | while (parts.length > 0) { 98 | var part = parts.shift(); 99 | var endIndex = part.match(/\?>|%>/).index; 100 | var embeddedCode = part.substring(0, endIndex); 101 | var constant = part.substring(endIndex + 2); 102 | 103 | if (/\s/.test(embeddedCode.charAt(0))) { 104 | jscode += "\n" + embeddedCode + "\n"; 105 | } else { 106 | var directFunction = directEvalFunction(embeddedCode) || defaultFunction(embeddedCode); 107 | if (typeof directFunction == "string") { 108 | jscode += "\n\t echo(" + directFunction + ");\n"; 109 | } else { 110 | directFunctions.push(directFunction); 111 | var argName = "fn" + Math.floor(Math.random()*10000000000); 112 | directFunctionVarNames.push(argName); 113 | jscode += "\n echo(" + argName + ".apply(this, _arguments));\n"; 114 | } 115 | } 116 | 117 | jscode += ' echo(' + JSON.stringify(constant) + ');\n'; 118 | } 119 | if (additionalParams['echo'] !== undefined) { 120 | jscode += '\n return "";\n})'; 121 | } else { 122 | jscode += '\n return ' + resultVariableName + ';\n})'; 123 | } 124 | 125 | //console.log("\n\n" + jscode + "\n\n"); 126 | 127 | var f = Function.apply(null, directFunctionVarNames.concat(["return " + jscode])); 128 | return f.apply(null, directFunctions); 129 | } 130 | 131 | function defaultFunction(varName) { 132 | return function (data) { 133 | var string = "" + data[varName]; 134 | return string.replace("&", "&").replace("<", "<").replace(">", "gt;").replace('"', """).replace("'", "'"); 135 | }; 136 | }; 137 | 138 | publicApi.loadTemplates = loadTemplates; 139 | publicApi.getTemplate = getTemplate; 140 | publicApi.create = create; 141 | publicApi.defaultFunction = defaultFunction; 142 | publicApi.defaultHeaderCode = "var value = arguments[0];"; 143 | })((typeof module !== 'undefined' && module.exports) ? exports : (this.jstl = {}, this.jstl)); 144 | 145 | // Jsonary plugin 146 | (function (Jsonary) { 147 | 148 | /* Template: jsonary-template-header-code 149 | var data = arguments[0], context = arguments[1]; 150 | function want(path) { 151 | var subData = data.subPath(path); 152 | return subData.defined() || !subData.readOnly(); 153 | }; 154 | function action(html, actionName) { 155 | echo(context.actionHtml.apply(context, arguments)); 156 | }; 157 | function render(subData, label) { 158 | echo(context.renderHtml(subData, label)); 159 | }; 160 | */ 161 | var headerCode = jstl.getTemplate('jsonary-template-header-code').code; 162 | var substitutionFunction = function (path) { 163 | if (path == "$") { 164 | return function (data, context) { 165 | return context.renderHtml(data); 166 | }; 167 | } else if (path.charAt(0) == "/") { 168 | return function (data, context) { 169 | return context.renderHtml(data.subPath(path)); 170 | }; 171 | } else if (path.charAt(0) == "=") { 172 | return 'window.escapeHtml(' + path.substring(1) + ')'; 173 | } else { 174 | return function (data, context) { 175 | var string = "" + data.propertyValue(path); 176 | return string.replace("&", "&").replace("<", "<").replace(">", "gt;").replace('"', """).replace("'", "'"); 177 | } 178 | } 179 | }; 180 | 181 | Jsonary.extend({ 182 | template: function (key) { 183 | var template = jstl.getTemplate(key); 184 | if (template == null) { 185 | throw new Exception("Could not locate template: " + key); 186 | } 187 | return template.compile(substitutionFunction, headerCode); 188 | }, 189 | loadTemplates: function () { 190 | jstl.loadTemplates(); 191 | } 192 | }); 193 | })(Jsonary); 194 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/basic.jsonary.css: -------------------------------------------------------------------------------- 1 | .json-schema, .json-link { 2 | margin-right: 0.5em; 3 | margin-left: 0.5em; 4 | border: 1px solid #DD3; 5 | background-color: #FFB; 6 | padding-left: 0.5em; 7 | padding-right: 0.5em; 8 | color: #880; 9 | font-size: 0.85em; 10 | font-style: italic; 11 | text-decoration: none; 12 | } 13 | 14 | .json-link { 15 | border: 1px solid #88F; 16 | background-color: #DDF; 17 | color: #008; 18 | font-style: normal; 19 | } 20 | 21 | .json-raw { 22 | display: inline; 23 | white-space: pre; 24 | } 25 | 26 | .valid { 27 | background-color: #DFD; 28 | } 29 | 30 | .invalid { 31 | background-color: #FDD; 32 | } 33 | 34 | textarea { 35 | vertical-align: middle; 36 | } 37 | 38 | /**** JSON Object ****/ 39 | 40 | .json-object { 41 | } 42 | 43 | .json-object-pair { 44 | } 45 | 46 | .json-object-key { 47 | text-align: right; 48 | vertical-align: top; 49 | font-style: italic; 50 | color: #840; 51 | padding-left: 2em; 52 | } 53 | 54 | .json-object-delete { 55 | font-family: monospace; 56 | font-style: normal; 57 | font-weight: bold; 58 | color: #F00; 59 | text-decoration: none; 60 | margin-right: 1em; 61 | } 62 | 63 | .json-object-add { 64 | display: block; 65 | padding-left: 2.2em; 66 | color: #888; 67 | font-size: 0.9em; 68 | } 69 | 70 | .json-object-add-key, .json-object-add-key-new { 71 | text-decoration: none; 72 | margin-left: 1em; 73 | color: #000; 74 | border: 1px solid #888; 75 | background-color: #EEE; 76 | } 77 | 78 | .json-object-add-key-new { 79 | border: 1px dotted #BBB; 80 | background-color: #EEF; 81 | font-style: italic; 82 | } 83 | 84 | .json-select-type-dialog-outer { 85 | position: relative; 86 | } 87 | 88 | .json-select-type-dialog { 89 | position: absolute; 90 | top: -0.65em; 91 | left: -0.5em; 92 | width: 8em; 93 | border: 2px solid black; 94 | border-radius: 10px; 95 | background-color: white; 96 | padding: 0.5em; 97 | z-index: 1; 98 | } 99 | 100 | .json-select-type-background { 101 | position: fixed; 102 | top: 0; 103 | right: 0; 104 | bottom: 0; 105 | left: 0; 106 | background-image: URL('images/background-stripe.png'); 107 | } 108 | 109 | .json-select-type { 110 | font-family: monospace; 111 | border: 1px solid #666; 112 | border-radius: 3px; 113 | color: #666; 114 | font-weight: bold; 115 | padding-left: 0.3em; 116 | padding-right: 0.3em; 117 | background-color: #DDD; 118 | background-image: URL('images/background-fade.png'); 119 | background-repeat: repeat-x; 120 | background-position: top; 121 | } 122 | 123 | .json-select-type:hover { 124 | background-image: URL('images/background-fade-bold.png'); 125 | } 126 | 127 | /**** JSON Array ****/ 128 | 129 | .json-array { 130 | } 131 | 132 | .json-array-item { 133 | display: block; 134 | padding-left: 2em; 135 | } 136 | 137 | .json-array-delete { 138 | font-family: monospace; 139 | font-style: normal; 140 | font-weight: bold; 141 | color: #F00; 142 | text-decoration: none; 143 | margin-right: 1em; 144 | } 145 | 146 | .json-array-add { 147 | display: block; 148 | padding-left: 2.2em; 149 | color: #000; 150 | 151 | font-family: monospace; 152 | font-style: normal; 153 | font-weight: bold; 154 | color: #00F; 155 | text-decoration: none; 156 | margin-right: 1em; 157 | } 158 | 159 | /**** String ****/ 160 | 161 | .json-string { 162 | white-space: pre; 163 | white-space: pre-wrap; 164 | border-radius: 3px; 165 | font-size: inherit; 166 | min-width: 5em; 167 | } 168 | 169 | .json-string-content-editable { 170 | display: inline; 171 | display: inline-block; 172 | vertical-align: text-top; 173 | background-color: #FFF; 174 | background-color: rgba(255, 255, 255, 0.95); 175 | outline: 1px solid #BBB; 176 | border-radius: 0px; 177 | color: #444; 178 | margin: 0.3em; 179 | padding: 0.3em; 180 | font-family: inherit; 181 | text-shadow: none; 182 | font-size: 0.9em; 183 | line-height: 1.2em; 184 | } 185 | 186 | .json-string-content-editable:focus { 187 | color: #000; 188 | border-color: #48C; 189 | box-shadow: 0px 0px 10px #000; 190 | } 191 | 192 | .json-string-content-editable p { 193 | display: block !important; 194 | margin: 0px !important; 195 | padding: 0px !important; 196 | } 197 | 198 | .json-string-content-editable * { 199 | position: static !important; 200 | margin: 0px !important; 201 | padding: 0px !important; 202 | font-size: inherit !important; 203 | font-family: inherit !important; 204 | color: #000 !important; 205 | background: none !important; 206 | border: none !important; 207 | outline: none !important; 208 | font-weight: normal !important; 209 | font-style: normal !important; 210 | text-decoration: none !important; 211 | text-transform: none !important; 212 | font-variant: normal !important; 213 | line-height: 1.2em !important; 214 | } 215 | 216 | textarea.json-string { 217 | font-size: inherit; 218 | font-weight: inherit; 219 | background-color: #FFF; 220 | border: 1px solid #000; 221 | background-color: rgba(255, 255, 255, 0.5); 222 | } 223 | 224 | .json-string-notice { 225 | color: #666; 226 | margin-left: 0.5em; 227 | } 228 | 229 | /**** Number ****/ 230 | 231 | .json-number { 232 | font-family: monospace; 233 | color: #000; 234 | font-weight: bold; 235 | text-decoration: none; 236 | } 237 | 238 | .json-number-increment, .json-number-decrement { 239 | font-family: monospace; 240 | color: #666; 241 | font-weight: bold; 242 | text-decoration: none; 243 | margin-left: 0.5em; 244 | margin-right: 0.5em; 245 | padding-left: 0.5em; 246 | padding-right: 0.5em; 247 | border: 1px solid #888; 248 | border-radius: 3px; 249 | background-color: #DDD; 250 | background-image: URL('images/background-fade.png'); 251 | background-repeat: repeat-x; 252 | background-position: top; 253 | } 254 | 255 | .json-number-increment:hover, .json-number-decrement:hover { 256 | background-image: URL('images/background-fade-bold.png'); 257 | } 258 | 259 | /**** Boolean ****/ 260 | 261 | .json-boolean-true, .json-boolean-false { 262 | font-family: monospace; 263 | color: #080; 264 | font-weight: bold; 265 | text-decoration: none; 266 | } 267 | 268 | .json-boolean-false { 269 | color: #800; 270 | } 271 | 272 | /**** Undefined ****/ 273 | 274 | .json-undefined-create { 275 | color: #008; 276 | text-decoration: none; 277 | } 278 | 279 | .json-undefined-create:hover { 280 | color: #08F; 281 | text-decoration: underline; 282 | } 283 | 284 | /**** Prompt ****/ 285 | 286 | .prompt-overlay { 287 | position: fixed; 288 | top: 0; 289 | left: 0; 290 | width: 70%; 291 | height: 100%; 292 | background-color: #000; 293 | padding-left: 15%; 294 | padding-right: 15%; 295 | background-color: rgba(100, 100, 100, 0.5); 296 | } 297 | 298 | .prompt-buttons { 299 | background-color: #EEE; 300 | border: 2px solid black; 301 | text-align: center; 302 | position: relative; 303 | } 304 | 305 | .prompt-data { 306 | background-color: white; 307 | border: 2px solid black; 308 | border-radius: 10px; 309 | position: relative; 310 | } 311 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/plain.jsonary.css: -------------------------------------------------------------------------------- 1 | .json-schema, .json-link { 2 | margin-right: 0.5em; 3 | margin-left: 0.5em; 4 | border: 1px solid #DD3; 5 | background-color: #FFB; 6 | padding-left: 0.5em; 7 | padding-right: 0.5em; 8 | color: #880; 9 | font-size: 0.85em; 10 | font-style: italic; 11 | text-decoration: none; 12 | } 13 | 14 | .json-link { 15 | border: 1px solid #88F; 16 | background-color: #DDF; 17 | color: #008; 18 | font-style: normal; 19 | } 20 | 21 | .json-raw { 22 | display: inline; 23 | white-space: pre; 24 | } 25 | 26 | .valid { 27 | background-color: #DFD; 28 | } 29 | 30 | .invalid { 31 | background-color: #FDD; 32 | } 33 | 34 | textarea { 35 | vertical-align: middle; 36 | } 37 | 38 | /**** JSON Object ****/ 39 | 40 | .json-object { 41 | width: 100%; 42 | } 43 | 44 | .json-object-title { 45 | font-weight: bold; 46 | } 47 | 48 | .json-object-outer { 49 | background-color: #FFF; 50 | border-radius: 3px; 51 | } 52 | 53 | .json-object-outer > legend { 54 | background-color: #EEE; 55 | border: 1px solid #BBB; 56 | border-radius: 3px; 57 | font-size: 0.8em; 58 | padding: 0.2em; 59 | padding-left: 0.7em; 60 | padding-right: 0.7em; 61 | } 62 | 63 | .json-object-pair { 64 | margin-bottom: 0.3em; 65 | } 66 | 67 | .json-object-key { 68 | padding: 0; 69 | vertical-align: top; 70 | width: 4em; 71 | } 72 | 73 | .json-object-key-text, .json-object-key-title { 74 | text-align: right; 75 | font-style: italic; 76 | padding-right: 0.5em; 77 | border-right: 1px solid #000; 78 | white-space: pre; 79 | } 80 | 81 | .json-object-key-title { 82 | font-weight: bold; 83 | font-style: normal; 84 | } 85 | 86 | .json-object-value { 87 | padding-left: 0.5em; 88 | vertical-align: top; 89 | } 90 | 91 | .json-object-delete-container { 92 | position: relative; 93 | vertical-align: top; 94 | padding-left: 1.2em; 95 | } 96 | 97 | .json-object-delete { 98 | position: absolute; 99 | left: 0; 100 | top: 0; 101 | font-family: monospace; 102 | font-style: normal; 103 | font-weight: bold; 104 | color: #F00; 105 | text-decoration: none; 106 | margin-right: 1em; 107 | } 108 | 109 | .json-object-delete-value { 110 | } 111 | 112 | .json-object-add { 113 | display: block; 114 | padding-left: 2.2em; 115 | color: #888; 116 | font-size: 0.9em; 117 | } 118 | 119 | .json-object-add-key, .json-object-add-key-new { 120 | text-decoration: none; 121 | margin-left: 1em; 122 | color: #000; 123 | border: 1px solid #888; 124 | background-color: #EEE; 125 | } 126 | 127 | .json-object-add-key-new { 128 | border: 1px dotted #BBB; 129 | background-color: #EEF; 130 | font-style: italic; 131 | } 132 | 133 | .json-select-type-dialog-outer { 134 | position: relative; 135 | } 136 | 137 | .json-select-type-dialog { 138 | position: absolute; 139 | top: -0.65em; 140 | left: -0.5em; 141 | width: 12em; 142 | border: 2px solid black; 143 | border-radius: 10px; 144 | background-color: white; 145 | padding: 0.5em; 146 | z-index: 1; 147 | } 148 | 149 | .json-select-type-background { 150 | position: fixed; 151 | top: 0; 152 | right: 0; 153 | bottom: 0; 154 | left: 0; 155 | background-image: URL('images/background-stripe.png'); 156 | } 157 | 158 | .json-select-type { 159 | font-family: monospace; 160 | border: 1px solid #666; 161 | border-radius: 3px; 162 | color: #666; 163 | font-weight: bold; 164 | padding-left: 0.3em; 165 | padding-right: 0.3em; 166 | background-color: #DDD; 167 | background-image: URL('images/background-fade.png'); 168 | background-repeat: repeat-x; 169 | background-position: top; 170 | } 171 | 172 | .json-select-type:hover { 173 | background-image: URL('images/background-fade-bold.png'); 174 | } 175 | 176 | /**** JSON Array ****/ 177 | 178 | .json-array { 179 | } 180 | 181 | .json-array-item { 182 | display: block; 183 | } 184 | 185 | .json-array-delete { 186 | font-family: monospace; 187 | font-style: normal; 188 | font-weight: bold; 189 | color: #F00; 190 | text-decoration: none; 191 | margin-right: 1em; 192 | } 193 | 194 | .json-array-add { 195 | display: block; 196 | padding-left: 2.2em; 197 | color: #000; 198 | 199 | font-family: monospace; 200 | font-style: normal; 201 | font-weight: bold; 202 | color: #00F; 203 | text-decoration: none; 204 | margin-right: 1em; 205 | } 206 | 207 | /**** String ****/ 208 | 209 | .json-string { 210 | white-space: pre-wrap; 211 | border-radius: 3px; 212 | font-size: inherit; 213 | } 214 | 215 | .json-string-content-editable { 216 | display: inline; 217 | display: inline-block; 218 | vertical-align: text-top; 219 | background-color: #FFF; 220 | background-color: rgba(255, 255, 255, 0.95); 221 | outline: 1px solid #BBB; 222 | outline: 1px solid rgba(0, 0, 0, 0.05); 223 | color: #444; 224 | margin: 0.1em; 225 | padding: 0.3em; 226 | font-family: inherit; 227 | text-shadow: none; 228 | font-size: 0.9em; 229 | line-height: 1.2em; 230 | min-width: 5em; 231 | min-height: 1.2em; 232 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 1); 233 | } 234 | 235 | .json-string-content-editable:focus { 236 | color: #000; 237 | border-color: #48C; 238 | box-shadow: 0px 0px 10px #000; 239 | z-index: 1; 240 | } 241 | 242 | .json-string-content-editable p { 243 | display: block !important; 244 | margin: 0px !important; 245 | padding: 0px !important; 246 | } 247 | 248 | .json-string-content-editable * { 249 | position: static !important; 250 | margin: 0px !important; 251 | padding: 0px !important; 252 | font-size: inherit !important; 253 | font-family: inherit !important; 254 | color: #000 !important; 255 | background: none !important; 256 | border: none !important; 257 | outline: none !important; 258 | font-weight: normal !important; 259 | font-style: normal !important; 260 | text-decoration: none !important; 261 | text-transform: none !important; 262 | font-variant: normal !important; 263 | line-height: 1.2em !important; 264 | } 265 | 266 | textarea.json-string { 267 | font-size: inherit; 268 | font-weight: inherit; 269 | background-color: #FFF; 270 | border: 1px solid #000; 271 | background-color: rgba(255, 255, 255, 0.5); 272 | width: 30%; 273 | } 274 | 275 | .json-string-notice { 276 | color: #666; 277 | margin-left: 0.5em; 278 | } 279 | 280 | /**** Number ****/ 281 | 282 | .json-number { 283 | font-family: monospace; 284 | color: #000; 285 | font-weight: bold; 286 | text-decoration: none; 287 | } 288 | 289 | .json-number-increment, .json-number-decrement { 290 | font-family: monospace; 291 | } 292 | 293 | /**** Boolean ****/ 294 | 295 | .json-boolean-true, .json-boolean-false { 296 | font-family: monospace; 297 | color: #080; 298 | font-weight: bold; 299 | text-decoration: none; 300 | } 301 | 302 | .json-boolean-false { 303 | color: #800; 304 | } 305 | 306 | /**** Undefined ****/ 307 | 308 | .json-undefined-create { 309 | color: #008; 310 | text-decoration: none; 311 | } 312 | 313 | .json-undefined-create:hover { 314 | color: #08F; 315 | text-decoration: underline; 316 | } 317 | 318 | /**** Prompt ****/ 319 | 320 | .prompt-overlay { 321 | position: fixed; 322 | top: 0; 323 | left: 0; 324 | width: 70%; 325 | height: 100%; 326 | background-color: #000; 327 | padding-left: 15%; 328 | padding-right: 15%; 329 | background-color: rgba(100, 100, 100, 0.5); 330 | } 331 | 332 | .prompt-buttons { 333 | background-color: #EEE; 334 | border: 2px solid black; 335 | text-align: center; 336 | position: relative; 337 | } 338 | 339 | .prompt-data { 340 | background-color: white; 341 | border: 2px solid black; 342 | border-radius: 10px; 343 | position: relative; 344 | } 345 | -------------------------------------------------------------------------------- /wild-ideas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Wild Ideas tracker 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
logged in as  
16 |

Wild Ideas tracker

17 |
18 |
19 | 24 | 25 |
26 |
27 | What is this? 28 |
29 |

This is a demo of how JsonStore and Jsonary can be used to quickly assemble an API and an interface. 30 |

It tracks ideas, because why not? 31 |

32 | 33 |
34 |
35 | ... 36 |
37 |
38 |
39 |
40 |
41 |
42 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 183 | 184 | -------------------------------------------------------------------------------- /wild-ideas/setup.php: -------------------------------------------------------------------------------- 1 | 'json/classes/idea.php' 7 | ); 8 | 9 | ?> 10 | 46 | 47 | "TEXT", 51 | "string" => "TEXT", 52 | "integer" => "INT(11)", 53 | "boolean" => "TINYINT(11)", 54 | "number" => "FLOAT" 55 | ); 56 | 57 | function checkConfig($config, $isArray=FALSE, $parentType=NULL) { 58 | global $mysqlTypeMap; 59 | $columns = array(); 60 | if ($isArray) { 61 | $groupColumn = (isset($config['alias']) && isset($config['alias']['group'])) ? $config['alias']['group'] : 'group'; 62 | $indexColumn = (isset($config['alias']) && isset($config['alias']['group'])) ? $config['alias']['index'] : 'index'; 63 | if (!$parentType) { 64 | $columns[$groupColumn] = JsonStore::escapedColumn($groupColumn)." INT(11) AUTO_INCREMENT COMMENT 'Array ID (generated)'"; 65 | } else if(isset($mysqlTypeMap[$type])) { 66 | $type = $mysqlTypeMap[$type]; 67 | $columns[$groupColumn] = JsonStore::escapedColumn($groupColumn)." {$type} COMMENT 'Link to parent'"; 68 | } else { 69 | $type = "INT(11) /*$type*/"; 70 | $columns[$groupColumn] = JsonStore::escapedColumn($groupColumn)." {$type} COMMENT 'Link to parent'"; 71 | } 72 | $columns[$indexColumn] = JsonStore::escapedColumn($indexColumn)." INT(11) COMMENT 'Array index'"; 73 | } 74 | foreach ($config['columns'] as $columnSpec => $columnName) { 75 | $subConfig = $columnName; 76 | if (is_numeric($columnSpec)) { 77 | $columnSpec = $columnName; 78 | } 79 | if (isset($config['alias']) && isset($config['alias'][$columnName])) { 80 | $columnName = $config['alias'][$columnName]; 81 | } 82 | $parts = explode("/", $columnSpec, 2); 83 | $type = $parts[0]; 84 | $path = substr($columnSpec, strlen($type)); 85 | $comment = $columnSpec; 86 | if ($type == "array") { 87 | $parentKeyType = NULL; 88 | if (isset($subConfig['parentKey'])) { 89 | foreach ($config['columns'] as $cs => $cn) { 90 | if (is_numeric($cs)) { 91 | $cs = $cn; 92 | } 93 | if ($cn == $subConfig['parentKey']) { 94 | $keyParts = explode("/", $cs, 2); 95 | $keyType = $keyParts[0]; 96 | $parentKeyType = $keyType; 97 | } 98 | } 99 | foreach ($config['alias'] as $cs => $cn) { 100 | if ($cn == $subConfig['parentKey']) { 101 | $keyParts = explode("/", $cs, 2); 102 | $keyType = $keyParts[0]; 103 | $parentKeyType = $keyType; 104 | } 105 | } 106 | checkConfig($subConfig, TRUE, $parentKeyType); 107 | continue; 108 | } else { 109 | checkConfig($subConfig, TRUE, NULL); 110 | $type == " INT(11)"; 111 | } 112 | } else if(isset($mysqlTypeMap[$type])) { 113 | $type = $mysqlTypeMap[$type]; 114 | } else { 115 | $type = "TEXT /*$type*/"; 116 | } 117 | if ($columnSpec == $config['keyColumn']) { 118 | $columns[$columnName] = JsonStore::escapedColumn($columnName)." {$type} AUTO_INCREMENT PRIMARY KEY COMMENT ".JsonStore::mysqlQuote($comment); 119 | } else { 120 | $columns[$columnName] = JsonStore::escapedColumn($columnName)." {$type} NULL COMMENT ".JsonStore::mysqlQuote($comment); 121 | } 122 | } 123 | 124 | if (isset($_POST['create-tables'])) { 125 | $sql = "CREATE TABLE IF NOT EXISTS {$config['table']} ("; 126 | $sql .= "\n\t".implode(",\n\t", $columns)."\n)"; 127 | echo "$sql;\n"; 128 | JsonStore::mysqlQuery($sql); 129 | } 130 | $result = JsonStore::mysqlQuery("SHOW COLUMNS FROM {$config['table']}"); 131 | $observedColumns = array(); 132 | foreach ($result as $column) { 133 | $observedColumns[$column['Field']] = $column; 134 | } 135 | if (isset($_POST['update-tables-add'])) { 136 | foreach ($columns as $columnName => $column) { 137 | if (!isset($observedColumns[$columnName])) { 138 | $sql = "ALTER TABLE {$config['table']}\n\tADD COLUMN ".$column; 139 | echo "$sql;\n"; 140 | JsonStore::mysqlQuery($sql); 141 | } 142 | } 143 | } 144 | if (isset($_POST['update-tables-delete'])) { 145 | foreach ($observedColumns as $columnName => $column) { 146 | if (!isset($columns[$columnName])) { 147 | $sql = "ALTER TABLE {$config['table']}\n\tDROP COLUMN ".$columnName; 148 | echo "$sql;\n"; 149 | JsonStore::mysqlQuery($sql); 150 | } 151 | } 152 | } 153 | } 154 | 155 | if (isset($_POST['setup'])) { 156 | echo '
';
157 | 
158 | 	foreach ($classes as $className => $filename) {
159 | 		$filename = str_replace("\\", "/", realpath(dirname($filename)))."/".basename($filename);
160 | 		if (!file_exists($filename)) {
161 | 			$filenameParts = explode("/", dirname($filename));
162 | 			$thisFileParts = explode("/", str_replace("\\", "/", __FILE__));
163 | 			while (count($filenameParts) && count($thisFileParts) && $filenameParts[0] == $thisFileParts[0]) {
164 | 				array_shift($filenameParts);
165 | 				array_shift($thisFileParts);
166 | 			}
167 | 			$thisFileParts[count($thisFileParts) - 1] = "";
168 | 			$includePath = str_repeat("../", count($filenameParts)) . implode("/", $thisFileParts) . $jsonStorePath;
169 | 		
170 | 			$result = NULL;
171 | 			if (isset($_POST['create-classes'])) {
172 | 				file_put_contents($filename, ' '.json_encode(strtolower($className)).',
179 | 	"keyColumn" => "integer/id",
180 | 	"columns" => array(
181 | 		"json" => "json",
182 | 		"integer/id" => "id"
183 | 	)
184 | ));
185 | ?>');
186 | 				echo "Created: $filename\n";
187 | 			}
188 | 			if (!$result) {
189 | 				echo '
File not found: '.htmlentities($filename).'
'; 190 | continue; 191 | } 192 | } 193 | require_once($filename); 194 | 195 | $config = JsonStore::getMysqlConfig($className); 196 | 197 | if (isset($_POST['update-classes'])) { 198 | $genFilename = str_replace('.php', '.gen.php', $filename); 199 | $code = file_get_contents($filename); 200 | if (!strpos($code, ".gen.php")) { 201 | $code = str_replace("class {$className} ", "require_once dirname(__FILE__).'".str_replace("'", '\\\'', "/".basename($genFilename))."';\n\nclass {$className} ", $code); 202 | } 203 | if (isset($config['keyColumn'])) { 204 | $keyColumnParts = explode('/', $config['keyColumn']); 205 | array_shift($keyColumnParts); 206 | $keyColumnCode = '->'.implode('->', $keyColumnParts); 207 | } 208 | $code = str_replace("{$className} extends JsonStore", "{$className} extends {$className}_gen", $code); 209 | file_put_contents($genFilename, ' \'ASC\'' : '').'); 217 | } 218 | $results = JsonStore::schemaSearch(\''.$className.'\', $schema, $orderBy); 219 | foreach ($results as $idx => $result) {' 220 | .(isset($config['keyColumn']) ? ' 221 | if ($cached = JsonStore::cached(\''.$className.'\', $result'.$keyColumnCode.')) { 222 | $results[$idx] = $cached; 223 | continue; 224 | } 225 | $results[$idx] = JsonStore.setCached(\''.$className.'\', $result'.$keyColumnCode.', new '.$className.'($result));' 226 | : ' 227 | $results[$idx] = new '.$className.'($result);' 228 | ).' 229 | } 230 | return $results; 231 | }' 232 | .(isset($config['keyColumn']) ? ' 233 | 234 | static public function open($id) { 235 | $model = newStdClass; 236 | $model'.$keyColumnCode.' = $id; 237 | $results = self::search(JsonSchema::fromModel($model)); 238 | return count($results) ? $results[0] : NULL; 239 | }' 240 | : '' 241 | ).' 242 | 243 | public function put($obj) {' 244 | .(isset($config['keyColumn']) ? ' 245 | $obj'.$keyColumnCode.' = $this'.$keyColumnCode.';' 246 | : '') 247 | .' 248 | foreach ($obj as $key => $value) { 249 | $this->$key = $value; 250 | } 251 | foreach ($this as $key => $value) { 252 | if (!isset($obj->$key)) { 253 | unset($this->$key); 254 | } 255 | } 256 | $this->save(); 257 | } 258 | 259 | public function get() { 260 | return $this; 261 | } 262 | } 263 | ?>') && file_put_contents($filename, $code); 264 | echo "Generated: $genFilename\n"; 265 | } 266 | 267 | checkConfig($config); 268 | } 269 | echo "\nDone."; 270 | echo '
'; 271 | die(); 272 | } 273 | ?> 274 |
275 |
276 | Code generation/updates 277 | 278 | 279 |
280 | 281 |
282 | 283 |
284 | MySQL structure 285 | 286 | 287 |
288 | 289 |
290 | 291 |
292 | 293 | 294 |
295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Store 2 | 3 | Storing JSON data with PHP/MySQL, as simply as possible. 4 | 5 | The idea is that the database tables can look completely normal - in fact, in many cases you can write configs for your existing table structure. 6 | 7 | Once you have supplied a config, then loading/saving/creation/deletion are all handled automatically. Searches can be performed using [JSON Schema](http://json-schema.org/) as a query language. 8 | 9 | It is **not** intended to be a full-featured ORM. It is simply a data-store, and intends to remain lightweight - however, an ORM could be built on top of it if needed. 10 | 11 | ### Example usage: 12 | 13 | ```php 14 | class MyClass extends JsonStore { 15 | static public function open($id) { 16 | $schema = new JsonSchema; 17 | $schema->properties->id->enum = array((int)$id); 18 | $results = self::search($schema); 19 | if (count($results) == 1) { 20 | return $results[0]; 21 | } 22 | } 23 | static public function search($schema) { 24 | $results = JsonStore::schemaSearch('MyClass', $schema, array("integer/id" => "ASC")); 25 | foreach ($results as $index => $item) { 26 | $results[$index] = new MyClass($item); 27 | } 28 | return $results; 29 | } 30 | static public function create($startingData) { 31 | $startingData = (object)$startingData; 32 | unset($startingData->id); // For safety 33 | if (!isset($startingData->source)) { 34 | $startingData->source = new StdClass; 35 | } else { 36 | $startingData->source = (object)$startingData->source; 37 | } 38 | return new MyClass($startingData); // constructor is protected 39 | } 40 | } 41 | JsonStore::addMysqlConfig('MyClass', array( 42 | "table" => "my_table_name", 43 | "keyColumn" => "integer/id", 44 | "columns" => array( 45 | "integer/id" => "id", 46 | "string/title" => "title", 47 | "string/source/url" => "source_url", 48 | "boolean/source/verified" => "source_verified" 49 | ) 50 | )); 51 | 52 | $myObj = MyClass::create(array( 53 | "title" => "Hello, world!", 54 | "source" => array( 55 | "url" => "http://example.com/", 56 | "verified" => FALSE 57 | ) 58 | )); 59 | isset($myObj->id); // FALSE 60 | $myObj->title; // "Hello, world!"; 61 | 62 | $myObj->save(); // performs an INSERT 63 | $myObj->id; // taken from the auto-increment in the database 64 | 65 | $myObj->source->verified = TRUE; 66 | $myObj->save(); // performs an UPDATE 67 | ``` 68 | 69 | ## Using the library 70 | 71 | You'll need to include `include/json-store.php`. 72 | 73 | You then just subclass `JsonStore`. The rules are: 74 | 75 | * The default constructor takes either: 76 | * a plain object (such as returned by `JsonStore::schemaSearch()`) 77 | * an associative array representing the database row (as returned by `$mysqli->fetch_assoc()` or `JsonStore::mysqlQuery()`) 78 | * You then make a call to `JsonStore::addMysqlConfig`, looking something like this: 79 | 80 | ```php 81 | JsonStore::addMysqlConfig('MyCustomClass', array( 82 | "table" => {{table name}}, 83 | "keyColumn" => "integer/id", 84 | "columns" => array(...) 85 | ); 86 | ``` 87 | 88 | It's a good idea to have either `"keyColumn"` (single value) or `"keyColumns"` (array representing a composite key). 89 | 90 | If `"keyColumn"` is present (and begins with "integer"), it is updated using the auto-increment value from the table. 91 | 92 | ## Table structure and column names 93 | 94 | Under the hood, JsonStore assumes that column names follow a particular pattern: `{type}{pointer}`. 95 | 96 | The `type` part of the column name denotes the JSON type to be stored there - one of "json" (raw JSON text), "integer", "number", "string" or "array". 97 | 98 | The remaining part of the column name is a JSON Pointer representing where that data maps to in the JSON object. For example, the column name `integer/id` 99 | 100 | ### Simple Example 101 | 102 | Say we are storing this data: 103 | 104 | ```json 105 | { 106 | "id": 1, 107 | "title": "Hello, World!", 108 | "someOtherProperty": [1, 2, 3] 109 | } 110 | ``` 111 | 112 | And say our columns are: 113 | 114 | * `json` 115 | * `integer/id` 116 | * `string/title` 117 | 118 | Then the table entry will look something like this: 119 | 120 | ``` 121 | ---------------------------------------------- 122 | | json | integer/id | string/title | 123 | ---------------------------------------------- 124 | | '{ ... }' | 1 | 'Hello, World!' | 125 | ---------------------------------------------- 126 | ``` 127 | 128 | ### Aliasing column names 129 | 130 | Because those column names are a bit messy, they can be aliased. To do this, simply use the JsonStore representation (e.g. "string/owner/name") as the key, and the actual column name as the value: 131 | ```php 132 | JsonStore::addMysqlConfig('MyCustomClass', array( 133 | "table" => 'MyTableName', 134 | "keyColumn" => "integer/id", 135 | "columns" => array( 136 | "integer/id" => "id", 137 | "string/owner/id" => "owner_id", 138 | "string/owner/name" => "owner_name" 139 | ) 140 | ); 141 | ``` 142 | 143 | This aliasing can also be specified using `"alias"` - the two are largely equivalent: 144 | ```php 145 | JsonStore::addMysqlConfig('MyCustomClass', array( 146 | "table" => 'MyTableName', 147 | "keyColumn" => "integer/id", 148 | "columns" => array("integer/id", ...), 149 | "alias" => array( 150 | "integer/id" => "id", 151 | ... 152 | ) 153 | ); 154 | ``` 155 | 156 | ## Circular references 157 | 158 | Don't use them. In fact, don't even reference JsonStore objects from each other. 159 | 160 | A better pattern is in fact to store an identifier in the object, and then define a method like this: 161 | 162 | ```php 163 | class MyClass extends JsonStore { 164 | public function open($id) { 165 | $sql = "SELECT * FROM my_table WHERE `integer/id`=".(int)$id; 166 | $rows = self::mysqlQuery($sql); 167 | if (count($rows)) { 168 | return new MyClass($rows[0]); // if you pass in an array, it inflates it into a full object 169 | } 170 | } 171 | 172 | public function parent() { 173 | return MyClass::open($this->parentId); 174 | } 175 | } 176 | ``` 177 | 178 | ## Arrays 179 | 180 | If an entry in `"columns"` is an array (i.e. the column name begins `array/...`), then the corresponding value should be itself be a config. The format is similar to above, with the following differences: 181 | 182 | * `"keyColumn"`/`"keyColumns"` will be ignored 183 | * If the optional parameter `"parentColumn"` is present, then this column (actual/aliased name, not the JsonStore internal one) is used to match against the array table. 184 | * There are two implied columns: `"group"` and `"index"`. These can be renamed just like any other column. 185 | * If `"parentColumn"` is specified, `"group"` must be of the same type. Otherwise, `"group"` must be an auto-incrementing integer, as must the `"array/..."` column in the original table. 186 | * `"index"` must be an integer type 187 | 188 | ### Basic config example 189 | 190 | Here is a basic example using an array from a separate table: 191 | ```php 192 | JsonStore::addMysqlConfig('MyCustomClass', array( 193 | "table" => 'my_table', 194 | "keyColumn" => "integer/id", 195 | "columns" => array( 196 | "integer/id", 197 | "string/title", 198 | "array/integerList" => array( 199 | "table" => 'my_table_integer_list', 200 | "columns" => array( 201 | "integer" 202 | ) 203 | ) 204 | ), 205 | ); 206 | ``` 207 | 208 | That example assumes the following columns for `my_table`: 209 | 210 | * `integer/id` - auto-incrementing integer 211 | * `string/title` - some string type 212 | * `array/integerList` - an integer 213 | 214 | and the following columns for `my_table_integer_list`: 215 | 216 | * `group` - auto-incrementing integer - will match up with the values in `array/integerList` 217 | * `index` - integer 218 | * `integer` - integer (represents the actual value at that index for that array) 219 | 220 | ### Complex array example 221 | 222 | Say we are storing this data: 223 | 224 | ```json 225 | { 226 | "id": 5, 227 | "title": "Hello!", 228 | "myArray": [ 229 | 1, 230 | 2, 231 | {"id": 3, "name": "three"} 232 | ] 233 | } 234 | ``` 235 | 236 | And say our config looks like this: 237 | 238 | ```php 239 | JsonStore::addMysqlConfig('MyCustomClass', array( 240 | "table" => "my_table", 241 | "keyColumn" => "integer/id", 242 | "columns" => array( 243 | "integer/id" => "id", 244 | "string/title" => "title", 245 | "array/myArray" => array( 246 | "table" => "my_array_items_table", 247 | "parentKey" => "id", 248 | "columns" => array( 249 | "group" => "parent_id", 250 | "index" => "pos", 251 | "integer" => "int_value", 252 | "integer/id" => "obj_id", 253 | "string/name" => "obj_name" 254 | ) 255 | ) 256 | ) 257 | ); 258 | ``` 259 | 260 | Then our main table (`my_table`) will look something like this: 261 | 262 | ``` 263 | -------------------------------- 264 | | id | title | 265 | -------------------------------- 266 | | 5 | "Hello!" | 267 | -------------------------------- 268 | ``` 269 | 270 | And our array table (`my_array_items_table`) will look something like this: 271 | 272 | ``` 273 | ------------------------------------------------------------- 274 | | parent_id | pos | int_value | obj_id | obj_name | 275 | ------------------------------------------------------------- 276 | | 5 | 0 | 1 | NULL | NULL | 277 | ------------------------------------------------------------- 278 | | 5 | 1 | 2 | NULL | NULL | 279 | ------------------------------------------------------------- 280 | | 5 | 2 | NULL | 3 | three | 281 | ------------------------------------------------------------- 282 | ``` 283 | 284 | Note that because we specified `"parentKey"` in the config, `my_table` doesn't need a separate column for the array ID. However, this means that there will *always* be an array in `$data->myArray`, it will simply be empty if there are no entries in `my_array_items_table`. 285 | 286 | ## Searching with JSON Schema 287 | 288 | JsonStore provides a helper class: JsonSchema. 289 | 290 | This class doesn't really do anything - it just makes it easier to assemble JSON Schemas by creating properties as they are needed. It also contains some shortcuts - for example, if you specify "properties", but haven't specified "type", then "type" will default to "object". 291 | 292 | ```php 293 | $schema = new JsonSchema(); 294 | $schema->properties->id = enum(5); // $schema->type has now defaulted to "object" 295 | ``` 296 | 297 | You will need to write your own static method for each of your classes, for example: 298 | 299 | ```php 300 | class MyClass extends JsonStore { 301 | public static function openAll($schema=NULL, $orderBy=NULL) { 302 | if (!$schema) { 303 | $schema = new JsonSchema(); // empty query 304 | } 305 | if (!$orderBy) { 306 | $orderBy = array("integer/id" => "ASC"); 307 | } 308 | $results = self::schemaSearch('MyClass', $schema, $orderBy); 309 | foreach ($results as $index => $item) { 310 | $results[$index] = new MyClass($item); // JsonStore::schemaSearch gives us an array of objects back 311 | } 312 | return $results; 313 | } 314 | } 315 | ``` 316 | 317 | `JsonStore::schemaSearch()` will convert the schema into an SQL query representing the same constraints. 318 | 319 | If there are any constraints that cannot be translated into the SQL query (because the database structure doesn't support them, or JsonStore doesn't recognise the keywords), then the results are filtered using the `jsv4-php` validation library before being returned. 320 | 321 | ## Caching loaded values 322 | 323 | Cacheing is not done automatically, however some convenience functions are provided. 324 | 325 | ```php 326 | class MyClass extends JsonStore { 327 | static public function open($id) { 328 | if ($cached = JsonStore::cached('MyClass', $id)) { 329 | return $cached; 330 | } 331 | $schema = new JsonSchema; 332 | $schema->properties->id->enum = array((int)$id); 333 | $results = self::search($schema); 334 | if (count($results) == 1) { 335 | return JsonStore::setCached('MyClass', $id, $results[0]); 336 | } 337 | } 338 | ... 339 | } 340 | ``` 341 | 342 | Similar things would obviously need to be done for search results as well. 343 | 344 | ## Precedence when loading data 345 | 346 | When loading data, values from more specific columns (longer paths) always take precedence. 347 | 348 | So for instance, given the following table: 349 | ``` 350 | ----------------------------------- 351 | | json/key | boolean/key/b | 352 | ----------------------------------- 353 | | '{"a":1,"b":2}' | 1 | 354 | ----------------------------------- 355 | ``` 356 | 357 | The data we will load will look like: 358 | ```json 359 | { 360 | "key": { 361 | "a": 1, 362 | "b": true 363 | } 364 | } 365 | ``` 366 | -------------------------------------------------------------------------------- /wild-ideas/style/grass/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, sans-serif; 3 | font-size: 13px; 4 | background-color: #C0C8B8; 5 | margin: 0; 6 | padding: 0; 7 | padding-bottom: 5em; 8 | 9 | transition: background-color 1s; 10 | } 11 | 12 | table, tr, td { 13 | font-size: inherit; 14 | } 15 | 16 | table { 17 | border-spacing: 0; 18 | border-collapse: collapse; 19 | } 20 | 21 | a[href] { 22 | color: #07D; 23 | text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.5); 24 | text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px -2px 2px rgba(255, 255, 255, 0.5); 25 | text-decoration: none; 26 | } 27 | 28 | a[href]:hover { 29 | color: #49F; 30 | text-shadow: 0px 0px 8px rgba(255, 255, 255, 0.8); 31 | } 32 | 33 | .loading { 34 | margin: 0; 35 | padding: 0; 36 | width: 100%; 37 | height: 30px; 38 | background-image: url('loading-small.gif'); 39 | background-position: top; 40 | background-repeat: no-repeat; 41 | } 42 | 43 | h1 { 44 | text-align: left; 45 | font-size: 1.3em; 46 | letter-spacing: 1px; 47 | margin: 0; 48 | padding-left: 0.5em; 49 | text-shadow: 0px 1px 4px #000; 50 | } 51 | 52 | #top-bar { 53 | position: relative; 54 | 55 | margin: 0; 56 | padding: 0.5em; 57 | border-bottom: 1px solid #000; 58 | 59 | box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.5); 60 | color: #FFF; 61 | } 62 | 63 | #top-bar a { 64 | color: #FFF; 65 | text-shadow: 0px 1px 4px #000; 66 | } 67 | 68 | #top-bar a:hover { 69 | text-shadow: 0px 1px 4px rgba(0, 0, 0, 0.5), 0px 1px 10px rgba(0, 0, 0, 0.5); 70 | } 71 | 72 | #content { 73 | max-width: 600px; 74 | margin: auto; 75 | padding-left: 1em; 76 | padding-right: 1em; 77 | } 78 | 79 | #about-us { 80 | font-size: 0.6em; 81 | display: block; 82 | text-align: right; 83 | position: absolute; 84 | width: 180px; 85 | top: 0.6em; 86 | right: 1em; 87 | transition: font-size 0.1s; 88 | } 89 | 90 | #page-menu { 91 | display: block; 92 | border: 1px solid #444; 93 | border-top-color: #888; 94 | border-bottom-left-radius: 5px; 95 | border-bottom-right-radius: 5px; 96 | margin: 0; 97 | padding: 0.5em; 98 | padding-left: 2em; 99 | box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1); 100 | opacity: 0.9; 101 | transition: box-shadow 2s; 102 | } 103 | 104 | #page-menu li.current a { 105 | color: #000; 106 | text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5), 0px -2px 2px #FFF; 107 | } 108 | 109 | #page-menu li a { 110 | text-decoration: none; 111 | } 112 | 113 | /** Login overlay **/ 114 | #login-overlay { 115 | position: fixed; 116 | z-index: 100000; 117 | left: 0; 118 | right: 0; 119 | top: 0; 120 | bottom: 0; 121 | background-color: #6F8B51; 122 | text-align: center; 123 | overflow: auto; 124 | } 125 | 126 | #login-box { 127 | position: relative; 128 | border: 2px solid black; 129 | border-radius: 10px; 130 | box-shadow: 0px 0px 20px rgba(210, 255, 200, 0.3), 0px 0px 50px rgba(210, 255, 200, 0.3); 131 | background-color: #F0F8E8; 132 | padding: 0.5em; 133 | padding-top: 2.5em; 134 | } 135 | 136 | #login-box-title { 137 | background-color: #B0B8A8; 138 | border-bottom: 2px solid black; 139 | border-top-left-radius: 9px; 140 | border-top-right-radius: 9px; 141 | text-align: center; 142 | position: absolute; 143 | left: 0; 144 | right: 0; 145 | top: 0; 146 | height: 1.3em; 147 | margin-bottom: 0.5em; 148 | padding: 0.2em; 149 | font-size: 1.1em; 150 | font-weight: bold; 151 | text-shadow: 0px 0px 5px rgba(255, 255, 255, 0.3); 152 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); 153 | } 154 | 155 | #login-box-message { 156 | font-weight: bold; 157 | color: #800; 158 | } 159 | 160 | #login-box-buttons { 161 | margin-top: 0.3em; 162 | } 163 | 164 | #login-box-buttons .button { 165 | padding: 0.2em; 166 | border-color: #000; 167 | color: #000; 168 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3); 169 | } 170 | 171 | @media screen and (min-width: 750px) { 172 | body { 173 | background-image: url('background.jpg'); 174 | background-position: top; 175 | background-repeat: no-repeat; 176 | } 177 | 178 | h1 { 179 | font-size: 1.6em; 180 | } 181 | } 182 | 183 | @media screen and (min-width: 600px) { 184 | body { 185 | background-color: #6F8B51; 186 | } 187 | 188 | #login-overlay { 189 | padding-top: 100px; 190 | opacity: 0.97; 191 | } 192 | 193 | #login-box { 194 | width: 400px; 195 | margin: auto; 196 | } 197 | 198 | 199 | h1 { 200 | text-align: center; 201 | padding-right: 150px; 202 | padding-left: 150px; 203 | } 204 | 205 | .loading { 206 | margin-top: 0.8em; 207 | height: 40px; 208 | background-image: url('loading.gif'); 209 | } 210 | 211 | span.loading { 212 | margin: 0; 213 | padding: 0; 214 | display: inline-block; 215 | height: 15px; 216 | width: 20px; 217 | background-size: 15px 15px; 218 | } 219 | 220 | #about-us { 221 | font-size: 1em; 222 | } 223 | 224 | #page-menu { 225 | border: none; 226 | border-top: 1px solid #000; 227 | border-bottom: 12px solid rgb(50, 120, 200); 228 | border-radius: 0; 229 | position: fixed; 230 | bottom: 0; 231 | left: 0; 232 | right: 0; 233 | text-align: center; 234 | background-color: #BCD; 235 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5); 236 | margin: 0; 237 | padding: 0; 238 | z-index: 100000; 239 | } 240 | 241 | #page-menu li { 242 | font-size: 1.1em; 243 | display: inline; 244 | display: inline-block; 245 | padding-left: 1em; 246 | padding-right: 1em; 247 | margin-left: 1em; 248 | margin-right: 1em; 249 | font-weight: bold; 250 | line-height: 2.1em; 251 | } 252 | 253 | #page-menu li.current { 254 | border-top: 2px solid black; 255 | border-bottom: 3px solid black; 256 | } 257 | } 258 | 259 | p { 260 | margin: 0.7em; 261 | } 262 | 263 | .box { 264 | border: 1px solid black; 265 | border-radius: 3px; 266 | border-top-left-radius: 15px; 267 | border-top-right-radius: 15px; 268 | text-align: left; 269 | margin: 0; 270 | margin-top: 1em; 271 | margin-bottom: 1em; 272 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1), 0px 3px 20px rgba(0, 0, 0, 0.05); 273 | } 274 | 275 | .title { 276 | border-bottom: 1px solid black; 277 | border-top-left-radius: 14px; 278 | border-top-right-radius: 14px; 279 | padding: 0.3em; 280 | text-align: center; 281 | box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3); 282 | color: #FFF; 283 | text-shadow: 0px 1px 4px #000; 284 | } 285 | 286 | .box .content { 287 | padding: 0.5em; 288 | } 289 | 290 | /** Tables **/ 291 | 292 | .json-array-table { 293 | width: 100%; 294 | } 295 | 296 | th { 297 | font-size: 0.9em; 298 | } 299 | 300 | .json-array-table > tbody > tr > td, .json-array-table > thead th { 301 | text-align: center; 302 | border: 1px solid #000; 303 | } 304 | 305 | .json-array-table > thead th a { 306 | margin-left: 1em; 307 | margin-right: 1em; 308 | color: #048; 309 | text-shadow: none; 310 | } 311 | 312 | .json-array-table > thead th a:hover { 313 | color: #036; 314 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 315 | } 316 | 317 | .json-array-table-sort-asc, .json-array-table-sort-desc { 318 | float: right; 319 | width: 0px; 320 | padding-left: 20px; 321 | margin-left: -20px; 322 | overflow: hidden; 323 | color: rgba(0, 0, 0, 0.5); 324 | background-repeat: no-repeat; 325 | background-position: top; 326 | border-radius: 10px; 327 | background-color: rgba(255, 255, 255, 0.5); 328 | box-shadow: 0px 0px 2px #FFF; 329 | } 330 | 331 | .json-array-table-sort-asc { 332 | background-image: url('sort-asc.png'); 333 | } 334 | 335 | .json-array-table-sort-desc { 336 | background-image: url('sort-desc.png'); 337 | } 338 | 339 | .json-array-table-full { 340 | padding: 1em; 341 | padding-top: 0.3em; 342 | background-color: #BCD; 343 | } 344 | 345 | .json-array-table-full-title { 346 | margin: -1em; 347 | margin-top: -0.3em; 348 | margin-bottom: 1em; 349 | background-color: #ABC; 350 | border-bottom: 1px solid #888; 351 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); 352 | font-weight: bold; 353 | padding: 0.2em; 354 | } 355 | 356 | /** Gradients **/ 357 | 358 | /* http://www.colorzilla.com/gradient-editor/#3d8de2+0,4eacf4+17,4096ee+31,3b78db+80,276bd8+100;Custom */ 359 | .blue-gradient, .json-array-table > thead th { 360 | background: rgb(61,141,226); /* Old browsers */ 361 | /* IE9 SVG, needs conditional override of 'filter' to 'none' */ 362 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzNkOGRlMiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjE3JSIgc3RvcC1jb2xvcj0iIzRlYWNmNCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjMxJSIgc3RvcC1jb2xvcj0iIzQwOTZlZSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjgwJSIgc3RvcC1jb2xvcj0iIzNiNzhkYiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMyNzZiZDgiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 363 | background: -moz-linear-gradient(top, rgba(61,141,226,1) 0%, rgba(78,172,244,1) 17%, rgba(64,150,238,1) 31%, rgba(59,120,219,1) 80%, rgba(39,107,216,1) 100%); /* FF3.6+ */ 364 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(61,141,226,1)), color-stop(17%,rgba(78,172,244,1)), color-stop(31%,rgba(64,150,238,1)), color-stop(80%,rgba(59,120,219,1)), color-stop(100%,rgba(39,107,216,1))); /* Chrome,Safari4+ */ 365 | background: -webkit-linear-gradient(top, rgba(61,141,226,1) 0%,rgba(78,172,244,1) 17%,rgba(64,150,238,1) 31%,rgba(59,120,219,1) 80%,rgba(39,107,216,1) 100%); /* Chrome10+,Safari5.1+ */ 366 | background: -o-linear-gradient(top, rgba(61,141,226,1) 0%,rgba(78,172,244,1) 17%,rgba(64,150,238,1) 31%,rgba(59,120,219,1) 80%,rgba(39,107,216,1) 100%); /* Opera 11.10+ */ 367 | background: -ms-linear-gradient(top, rgba(61,141,226,1) 0%,rgba(78,172,244,1) 17%,rgba(64,150,238,1) 31%,rgba(59,120,219,1) 80%,rgba(39,107,216,1) 100%); /* IE10+ */ 368 | background: linear-gradient(to bottom, rgba(61,141,226,1) 0%,rgba(78,172,244,1) 17%,rgba(64,150,238,1) 31%,rgba(59,120,219,1) 80%,rgba(39,107,216,1) 100%); /* W3C */ 369 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3d8de2', endColorstr='#276bd8',GradientType=0 ); /* IE6-8 */ 370 | } 371 | 372 | /* http://www.colorzilla.com/gradient-editor/#ffffff+0,ededed+100;Custom */ 373 | .diagonal-gradient, .json-array-table { 374 | /* IE9 SVG, needs conditional override of 'filter' to 'none' */ 375 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iI2Y2ZjZmNiIgc3RvcC1vcGFjaXR5PSIwLjk1Ii8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNlZGVkZWQiIHN0b3Atb3BhY2l0eT0iMC44Ii8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); 376 | background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(246,246,246,0.95) 50%, rgba(237,237,237,0.8) 100%); /* FF3.6+ */ 377 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(50%,rgba(246,246,246,0.95)), color-stop(100%,rgba(237,237,237,0.8))); /* Chrome,Safari4+ */ 378 | background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(246,246,246,0.95) 50%,rgba(237,237,237,0.8) 100%); /* Chrome10+,Safari5.1+ */ 379 | background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(246,246,246,0.95) 50%,rgba(237,237,237,0.8) 100%); /* Opera 11.10+ */ 380 | background: -ms-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(246,246,246,0.95) 50%,rgba(237,237,237,0.8) 100%); /* IE10+ */ 381 | background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(246,246,246,0.95) 50%,rgba(237,237,237,0.8) 100%); /* W3C */ 382 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ccededed',GradientType=0 ); /* IE6-8 */ 383 | } 384 | 385 | /* http://www.colorzilla.com/gradient-editor/#bfe4fc+0,86b9e0+100;Custom */ 386 | .gentle-blue-gradient { 387 | background: rgb(234,240,244); /* Old browsers */ 388 | /* IE9 SVG, needs conditional override of 'filter' to 'none' */ 389 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VhZjBmNCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNkMmRiZTIiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 390 | background: -moz-linear-gradient(top, rgba(234,240,244,1) 0%, rgba(210,219,226,1) 100%); /* FF3.6+ */ 391 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(234,240,244,1)), color-stop(100%,rgba(210,219,226,1))); /* Chrome,Safari4+ */ 392 | background: -webkit-linear-gradient(top, rgba(234,240,244,1) 0%,rgba(210,219,226,1) 100%); /* Chrome10+,Safari5.1+ */ 393 | background: -o-linear-gradient(top, rgba(234,240,244,1) 0%,rgba(210,219,226,1) 100%); /* Opera 11.10+ */ 394 | background: -ms-linear-gradient(top, rgba(234,240,244,1) 0%,rgba(210,219,226,1) 100%); /* IE10+ */ 395 | background: linear-gradient(to bottom, rgba(234,240,244,1) 0%,rgba(210,219,226,1) 100%); /* W3C */ 396 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eaf0f4', endColorstr='#d2dbe2',GradientType=0 ); /* IE6-8 */ 397 | } -------------------------------------------------------------------------------- /include/jsv4.php: -------------------------------------------------------------------------------- 1 | valid; 39 | } 40 | 41 | static public function coerce($data, $schema) { 42 | if (is_object($data) || is_array($data)) { 43 | $data = unserialize(serialize($data)); 44 | } 45 | $result = new Jsv4($data, $schema, FALSE, TRUE); 46 | if ($result->valid) { 47 | $result->value = $result->data; 48 | } 49 | return $result; 50 | } 51 | 52 | static public function pointerJoin($parts) { 53 | $result = ""; 54 | foreach ($parts as $part) { 55 | $part = str_replace("~", "~0", $part); 56 | $part = str_replace("/", "~1", $part); 57 | $result .= "/".$part; 58 | } 59 | return $result; 60 | } 61 | 62 | static public function recursiveEqual($a, $b) { 63 | if (is_object($a)) { 64 | if (!is_object($b)) { 65 | return FALSE; 66 | } 67 | foreach ($a as $key => $value) { 68 | if (!isset($b->$key)) { 69 | return FALSE; 70 | } 71 | if (!recursiveEqual($value, $b->$key)) { 72 | return FALSE; 73 | } 74 | } 75 | foreach ($b as $key => $value) { 76 | if (!isset($a->$key)) { 77 | return FALSE; 78 | } 79 | } 80 | return TRUE; 81 | } 82 | if (is_array($a)) { 83 | if (!is_array($b)) { 84 | return FALSE; 85 | } 86 | foreach ($a as $key => $value) { 87 | if (!isset($b[$key])) { 88 | return FALSE; 89 | } 90 | if (!recursiveEqual($value, $b[$key])) { 91 | return FALSE; 92 | } 93 | } 94 | foreach ($b as $key => $value) { 95 | if (!isset($a[$key])) { 96 | return FALSE; 97 | } 98 | } 99 | return TRUE; 100 | } 101 | return $a === $b; 102 | } 103 | 104 | 105 | private $data; 106 | private $schema; 107 | private $firstErrorOnly; 108 | private $coerce; 109 | public $valid; 110 | public $errors; 111 | 112 | private function __construct(&$data, $schema, $firstErrorOnly=FALSE, $coerce=FALSE) { 113 | $this->data =& $data; 114 | $this->schema =& $schema; 115 | $this->firstErrorOnly = $firstErrorOnly; 116 | $this->coerce = $coerce; 117 | $this->valid = TRUE; 118 | $this->errors = array(); 119 | 120 | try { 121 | $this->checkTypes(); 122 | $this->checkEnum(); 123 | $this->checkObject(); 124 | $this->checkArray(); 125 | $this->checkString(); 126 | $this->checkNumber(); 127 | $this->checkComposite(); 128 | } catch (Jsv4Error $e) { 129 | } 130 | } 131 | 132 | private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors=NULL) { 133 | $this->valid = FALSE; 134 | $error = new Jsv4Error($code, $dataPath, $schemaPath, $errorMessage, $subErrors); 135 | $this->errors[] = $error; 136 | if ($this->firstErrorOnly) { 137 | throw $error; 138 | } 139 | } 140 | 141 | private function subResult(&$data, $schema, $allowCoercion=TRUE) { 142 | return new Jsv4($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); 143 | } 144 | 145 | private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) { 146 | if (!$subResult->valid) { 147 | $this->valid = FALSE; 148 | foreach ($subResult->errors as $error) { 149 | $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); 150 | } 151 | } 152 | } 153 | 154 | private function checkTypes() { 155 | if (isset($this->schema->type)) { 156 | $types = $this->schema->type; 157 | if (!is_array($types)) { 158 | $types = array($types); 159 | } 160 | foreach ($types as $type) { 161 | if ($type == "object" && is_object($this->data)) { 162 | return; 163 | } elseif ($type == "array" && is_array($this->data)) { 164 | return; 165 | } elseif ($type == "string" && is_string($this->data)) { 166 | return; 167 | } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { 168 | return; 169 | } elseif ($type == "integer" && is_int($this->data)) { 170 | return; 171 | } elseif ($type == "boolean" && is_bool($this->data)) { 172 | return; 173 | } elseif ($type == "null" && $this->data === NULL) { 174 | return; 175 | } 176 | } 177 | 178 | if ($this->coerce) { 179 | foreach ($types as $type) { 180 | if ($type == "number") { 181 | if (is_numeric($this->data)) { 182 | $this->data = (float)$this->data; 183 | return; 184 | } else if (is_bool($this->data)) { 185 | $this->data = $this->data ? 1 : 0; 186 | return; 187 | } 188 | } else if ($type == "integer") { 189 | if ((int)$this->data == $this->data) { 190 | $this->data = (int)$this->data; 191 | return; 192 | } 193 | } else if ($type == "string") { 194 | if (is_numeric($this->data)) { 195 | $this->data = "".$this->data; 196 | return; 197 | } else if (is_bool($this->data)) { 198 | $this->data = ($this->data) ? "true" : "false"; 199 | return; 200 | } else if (is_null($this->data)) { 201 | $this->data = ""; 202 | return; 203 | } 204 | } else if ($type == "boolean") { 205 | if (is_numeric($this->data)) { 206 | $this->data = ($this->data != "0"); 207 | return; 208 | } else if ($this->data == "yes" || $this->data == "true") { 209 | $this->data = TRUE; 210 | return; 211 | } else if ($this->data == "no" || $this->data == "false") { 212 | $this->data = FALSE; 213 | return; 214 | } else if ($this->data == NULL) { 215 | $this->data = FALSE; 216 | return; 217 | } 218 | } 219 | } 220 | } 221 | 222 | $type = gettype($this->data); 223 | if ($type == "double") { 224 | $type = ((int)$this->data == $this->data) ? "integer" : "number"; 225 | } else if ($type == "NULL") { 226 | $type = "null"; 227 | } 228 | $this->fail(JSV4_INVALID_TYPE, "", "/type", "Invalid type: $type"); 229 | } 230 | } 231 | 232 | private function checkEnum() { 233 | if (isset($this->schema->enum)) { 234 | foreach ($this->schema->enum as $option) { 235 | if (self::recursiveEqual($this->data, $option)) { 236 | return; 237 | } 238 | } 239 | $this->fail(JSV4_ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); 240 | } 241 | } 242 | 243 | private function checkObject() { 244 | if (!is_object($this->data)) { 245 | return; 246 | } 247 | if (isset($this->schema->required)) { 248 | foreach ($this->schema->required as $index => $key) { 249 | if (!isset($this->data->$key)) { 250 | if ($this->coerce && $this->createValueForProperty($key)) { 251 | continue; 252 | } 253 | $this->fail(JSV4_OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); 254 | } 255 | } 256 | } 257 | $checkedProperties = array(); 258 | if (isset($this->schema->properties)) { 259 | foreach ($this->schema->properties as $key => $subSchema) { 260 | $checkedProperties[$key] = TRUE; 261 | if (isset($this->data->$key)) { 262 | $subResult = $this->subResult($this->data->$key, $subSchema); 263 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("properties", $key))); 264 | } 265 | } 266 | } 267 | if (isset($this->schema->patternProperties)) { 268 | foreach ($this->schema->patternProperties as $pattern => $subSchema) { 269 | foreach ($this->data as $key => &$subValue) { 270 | if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { 271 | $checkedProperties[$key] = TRUE; 272 | $subResult = $this->subResult($this->data->$key, $subSchema); 273 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("patternProperties", $pattern))); 274 | } 275 | } 276 | } 277 | } 278 | if (isset($this->schema->additionalProperties)) { 279 | $additionalProperties = $this->schema->additionalProperties; 280 | foreach ($this->data as $key => &$subValue) { 281 | if (isset($checkedProperties[$key])) { 282 | continue; 283 | } 284 | if (!$additionalProperties) { 285 | $this->fail(JSV4_OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); 286 | } else if (is_object($additionalProperties)) { 287 | $subResult = $this->subResult($subValue, $additionalProperties); 288 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); 289 | } 290 | } 291 | } 292 | if (isset($this->schema->dependencies)) { 293 | foreach ($this->schema->dependencies as $key => $dep) { 294 | if (!isset($this->data->$key)) { 295 | continue; 296 | } 297 | if (is_object($dep)) { 298 | $subResult = $this->subResult($this->data, $dep); 299 | $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); 300 | } else if (is_array($dep)) { 301 | foreach ($dep as $index => $depKey) { 302 | if (!isset($this->data->$depKey)) { 303 | $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); 304 | } 305 | } 306 | } else { 307 | if (!isset($this->data->$dep)) { 308 | $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); 309 | } 310 | } 311 | } 312 | } 313 | if (isset($this->schema->minProperties)) { 314 | if (count(get_object_vars($this->data)) < $this->schema->minProperties) { 315 | $this->fail(JSV4_OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); 316 | } 317 | } 318 | if (isset($this->schema->maxProperties)) { 319 | if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { 320 | $this->fail(JSV4_OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); 321 | } 322 | } 323 | } 324 | 325 | private function checkArray() { 326 | if (!is_array($this->data)) { 327 | return; 328 | } 329 | if (isset($this->schema->items)) { 330 | $items = $this->schema->items; 331 | if (is_array($items)) { 332 | foreach ($this->data as $index => &$subData) { 333 | if (!is_numeric($index)) { 334 | throw new Exception("Arrays must only be numerically-indexed"); 335 | } 336 | if (isset($items[$index])) { 337 | $subResult = $this->subResult($subData, $items[$index]); 338 | $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); 339 | } else if (isset($this->schema->additionalItems)) { 340 | $additionalItems = $this->schema->additionalItems; 341 | if (!$additionalItems) { 342 | $this->fail(JSV4_ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index ".count($items)." or more) are not allowed"); 343 | } else if ($additionalItems !== TRUE) { 344 | $subResult = $this->subResult($subData, $additionalItems); 345 | $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); 346 | } 347 | } 348 | } 349 | } else { 350 | foreach ($this->data as $index => &$subData) { 351 | if (!is_numeric($index)) { 352 | throw new Exception("Arrays must only be numerically-indexed"); 353 | } 354 | $subResult = $this->subResult($subData, $items); 355 | $this->includeSubResult($subResult, "/{$index}", "/items"); 356 | } 357 | } 358 | } 359 | if (isset($this->schema->minItems)) { 360 | if (count($this->data) < $this->schema->minItems) { 361 | $this->fail(JSV4_ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); 362 | } 363 | } 364 | if (isset($this->schema->maxItems)) { 365 | if (count($this->data) > $this->schema->maxItems) { 366 | $this->fail(JSV4_ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); 367 | } 368 | } 369 | if (isset($this->schema->uniqueItems)) { 370 | foreach ($this->data as $indexA => $itemA) { 371 | foreach ($this->data as $indexB => $itemB) { 372 | if ($indexA < $indexB) { 373 | if (self::recursiveEqual($itemA, $itemB)) { 374 | $this->fail(JSV4_ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); 375 | break 2; 376 | } 377 | } 378 | } 379 | } 380 | } 381 | } 382 | 383 | private function checkString() { 384 | if (!is_string($this->data)) { 385 | return; 386 | } 387 | if (isset($this->schema->minLength)) { 388 | if (strlen($this->data) < $this->schema->minLength) { 389 | $this->fail(JSV4_STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); 390 | } 391 | } 392 | if (isset($this->schema->maxLength)) { 393 | if (strlen($this->data) > $this->schema->maxLength) { 394 | $this->fail(JSV4_STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); 395 | } 396 | } 397 | if (isset($this->schema->pattern)) { 398 | $pattern = $this->schema->pattern; 399 | $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; 400 | $result = preg_match("/".str_replace("/", "\\/", $pattern)."/".$patternFlags, $this->data); 401 | if ($result === 0) { 402 | $this->fail(JSV4_STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); 403 | } 404 | } 405 | } 406 | 407 | private function checkNumber() { 408 | if (is_string($this->data) || !is_numeric($this->data)) { 409 | return; 410 | } 411 | if (isset($this->schema->multipleOf)) { 412 | if (fmod($this->data, $this->schema->multipleOf) != 0) { 413 | $this->fail(JSV4_NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); 414 | } 415 | } 416 | if (isset($this->schema->minimum)) { 417 | $minimum = $this->schema->minimum; 418 | if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { 419 | if ($this->data <= $minimum) { 420 | $this->fail(JSV4_NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); 421 | } 422 | } else { 423 | if ($this->data < $minimum) { 424 | $this->fail(JSV4_NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); 425 | } 426 | } 427 | } 428 | if (isset($this->schema->maximum)) { 429 | $maximum = $this->schema->maximum; 430 | if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { 431 | if ($this->data >= $maximum) { 432 | $this->fail(JSV4_NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); 433 | } 434 | } else { 435 | if ($this->data > $maximum) { 436 | $this->fail(JSV4_NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); 437 | } 438 | } 439 | } 440 | } 441 | 442 | private function checkComposite() { 443 | if (isset($this->schema->allOf)) { 444 | foreach ($this->schema->allOf as $index => $subSchema) { 445 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 446 | $this->includeSubResult($subResult, "", "/allOf/".(int)$index); 447 | } 448 | } 449 | if (isset($this->schema->anyOf)) { 450 | $failResults = array(); 451 | foreach ($this->schema->anyOf as $index => $subSchema) { 452 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 453 | if ($subResult->valid) { 454 | return; 455 | } 456 | $failResults[] = $subResult; 457 | } 458 | $this->fail(JSV4_ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); 459 | } 460 | if (isset($this->schema->oneOf)) { 461 | $failResults = array(); 462 | $successIndex = NULL; 463 | foreach ($this->schema->oneOf as $index => $subSchema) { 464 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 465 | if ($subResult->valid) { 466 | if ($successIndex === NULL) { 467 | $successIndex = $index; 468 | } else { 469 | $this->fail(JSV4_ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); 470 | } 471 | continue; 472 | } 473 | $failResults[] = $subResult; 474 | } 475 | if ($successIndex === NULL) { 476 | $this->fail(JSV4_ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); 477 | } 478 | } 479 | if (isset($this->schema->not)) { 480 | $subResult = $this->subResult($this->data, $this->schema->not, FALSE); 481 | if ($subResult->valid) { 482 | $this->fail(JSV4_NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); 483 | } 484 | } 485 | } 486 | 487 | private function createValueForProperty($key) { 488 | $schema = NULL; 489 | if (isset($this->schema->properties->$key)) { 490 | $schema = $this->schema->properties->$key; 491 | } else if (isset($this->schema->patternProperties)) { 492 | foreach ($this->schema->patternProperties as $pattern => $subSchema) { 493 | if (preg_match("/".str_replace("/", "\\/", $pattern)."/")) { 494 | $schema = $subSchema; 495 | break; 496 | } 497 | } 498 | } 499 | if (!$schema && isset($this->schema->additionalProperties)) { 500 | $schema = $this->schema->additionalProperties; 501 | } 502 | if ($schema) { 503 | if (isset($schema->default)) { 504 | $this->data->$key = unserialize(serialize($schema->default)); 505 | return TRUE; 506 | } 507 | if (isset($schema->type)) { 508 | $types = is_array($schema->type) ? $schema->type : array($schema->type); 509 | if (in_array("null", $types)) { 510 | $this->data->$key = NULL; 511 | } elseif (in_array("boolean", $types)) { 512 | $this->data->$key = TRUE; 513 | } elseif (in_array("integer", $types) || in_array("number", $types)) { 514 | $this->data->$key = 0; 515 | } elseif (in_array("string", $types)) { 516 | $this->data->$key = ""; 517 | } elseif (in_array("object", $types)) { 518 | $this->data->$key = new StdClass; 519 | } elseif (in_array("array", $types)) { 520 | $this->data->$key = array(); 521 | } else { 522 | return FALSE; 523 | } 524 | } 525 | return TRUE; 526 | } 527 | return FALSE; 528 | } 529 | } 530 | 531 | class Jsv4Error extends Exception { 532 | public $code; 533 | public $dataPath; 534 | public $schemaPath; 535 | public $message; 536 | 537 | public function __construct($code, $dataPath, $schemaPath, $errorMessage, $subResults=NULL) { 538 | parent::__construct($errorMessage); 539 | $this->code = $code; 540 | $this->dataPath = $dataPath; 541 | $this->schemaPath = $schemaPath; 542 | $this->message = $errorMessage; 543 | if ($subResults) { 544 | $this->subResults = $subResults; 545 | } 546 | } 547 | 548 | public function prefix($dataPrefix, $schemaPrefix) { 549 | return new Jsv4Error($this->code, $dataPrefix.$this->dataPath, $schemaPrefix.$this->schemaPath, $this->message); 550 | } 551 | } 552 | 553 | ?> -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/schemas.jsonary.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | var knownSchemaKeys = ["title", "description", "type", "enum", "default", "allOf", "anyOf", "oneOf", "not", "multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "required", "properties", "patternProperties", "additionalProperties", "minProperties", "maxProperties", "dependencies", "items", "additionalItems", "maxItems", "minItems", "uniqueItems", "definitions"]; 3 | 4 | Jsonary.render.register({ 5 | tabs: { 6 | all: { 7 | title: "Univeral constraints", 8 | renderHtml: function (data, context) { 9 | var result = ""; 10 | if (!data.readOnly() || data.property("enum").defined()) { 11 | result += '

Enum values:

'; 12 | result += '
' + context.renderHtml(data.property("enum")) + '
'; 13 | } 14 | if (!data.readOnly() || data.property("default").defined()) { 15 | result += '

Default value:

'; 16 | result += '
' + context.renderHtml(data.property("default")) + '
'; 17 | } 18 | if (!data.readOnly() || data.property("allOf").defined()) { 19 | result += '

All of (extends):

'; 20 | result += '
' + context.renderHtml(data.property("allOf")) + '
'; 21 | } 22 | if (!data.readOnly() || data.property("anyOf").defined()) { 23 | result += '

At least one of:

'; 24 | result += '
' + context.renderHtml(data.property("anyOf")) + '
'; 25 | } 26 | if (!data.readOnly() || data.property("oneOf").defined()) { 27 | result += '

Exactly one of:

'; 28 | result += '
' + context.renderHtml(data.property("oneOf")) + '
'; 29 | } 30 | if (!data.readOnly() || data.property("not").defined()) { 31 | result += '

Must not be:

'; 32 | result += '
' + context.renderHtml(data.property("not")) + '
'; 33 | } 34 | return result; 35 | } 36 | }, 37 | number: { 38 | title: "Number", 39 | renderHtml: function (data, context) { 40 | var result = ""; 41 | if (!data.readOnly() || data.property("multipleOf").defined()) { 42 | result += '

Multiple of:

'; 43 | result += '
' + context.renderHtml(data.property("multipleOf")) + '
'; 44 | } 45 | if (!data.readOnly() || data.property("maximum").defined()) { 46 | result += '

Maximum:

'; 47 | result += '
' + context.renderHtml(data.property("maximum")) + '
'; 48 | if (data.property("maximum").defined()) { 49 | result += '

Exlusive maximum:

'; 50 | result += '
' + context.renderHtml(data.property("exclusiveMaximum")) + '
'; 51 | } 52 | } 53 | if (!data.readOnly() || data.property("minimum").defined()) { 54 | result += '

Minimum:

'; 55 | result += '
' + context.renderHtml(data.property("minimum")) + '
'; 56 | if (data.property("minimum").defined()) { 57 | result += '

Exlusive minimum:

'; 58 | result += '
' + context.renderHtml(data.property("exclusiveMinimum")) + '
'; 59 | } 60 | } 61 | return result; 62 | } 63 | }, 64 | string: { 65 | title: "String", 66 | renderHtml: function (data, context) { 67 | var result = ""; 68 | if (!data.readOnly() || data.property("minLength").defined()) { 69 | result += '

Minimum length:

'; 70 | result += '
' + context.renderHtml(data.property("minLength")) + '
'; 71 | } 72 | if (!data.readOnly() || data.property("maxLength").defined()) { 73 | result += '

Maximum length:

'; 74 | result += '
' + context.renderHtml(data.property("maxLength")) + '
'; 75 | } 76 | if (!data.readOnly() || data.property("pattern").defined()) { 77 | result += '

Regular expression pattern:

'; 78 | result += '
' + context.renderHtml(data.property("pattern")) + '
'; 79 | } 80 | return result; 81 | } 82 | }, 83 | object: { 84 | title: "Object", 85 | renderHtml: function (data, context) { 86 | var result = ""; 87 | if (!data.readOnly() || data.property("required").defined()) { 88 | result += '

Required properties:

'; 89 | result += '
' + context.renderHtml(data.property("required")) + '
'; 90 | } 91 | result += '

Properties:

'; 92 | result += '
' + context.renderHtml(data.property("properties")) + '
'; 93 | if (!data.readOnly() || data.property("patternProperties").defined()) { 94 | result += '

Pattern properties:

'; 95 | result += '
' + context.renderHtml(data.property("patternProperties")) + '
'; 96 | } 97 | if (!data.readOnly() || data.property("additionalProperties").defined()) { 98 | result += '

All other properties:

'; 99 | result += '
' + context.renderHtml(data.property("additionalProperties")) + '
'; 100 | } 101 | if (!data.readOnly() || data.property("minProperties").defined()) { 102 | result += '

Minimum number of properties:

'; 103 | result += '
' + context.renderHtml(data.property("minProperties")) + '
'; 104 | } 105 | if (!data.readOnly() || data.property("maxProperties").defined()) { 106 | result += '

Maximum number of properties:

'; 107 | result += '
' + context.renderHtml(data.property("maxProperties")) + '
'; 108 | } 109 | if (!data.readOnly() || data.property("dependencies").defined()) { 110 | result += '

Property dependencies:

'; 111 | result += '
' + context.renderHtml(data.property("dependencies")) + '
'; 112 | } 113 | return result; 114 | } 115 | }, 116 | array: { 117 | title: "Array", 118 | renderHtml: function (data, context) { 119 | var result = ""; 120 | result += '

Items:

'; 121 | result += '
' + context.renderHtml(data.property("items")) + '
'; 122 | if (data.property("items").basicType() == "array") { 123 | if (!data.readOnly() || data.property("additionalItems").defined()) { 124 | result += '

Additional items:

'; 125 | result += '
' + context.renderHtml(data.property("additionalItems")) + '
'; 126 | } 127 | } 128 | if (!data.readOnly() || data.property("maxItems").defined()) { 129 | result += '

Maximum length:

'; 130 | result += '
' + context.renderHtml(data.property("maxItems")) + '
'; 131 | } 132 | if (!data.readOnly() || data.property("minItems").defined()) { 133 | result += '

Minimum length:

'; 134 | result += '
' + context.renderHtml(data.property("minItems")) + '
'; 135 | } 136 | if (!data.readOnly() || data.property("uniqueItems").defined()) { 137 | result += '

Unique:

'; 138 | result += '
' + context.renderHtml(data.property("uniqueItems")) + '
'; 139 | } 140 | return result; 141 | } 142 | }, 143 | definitions: { 144 | title: "Definitions", 145 | renderHtml: function (data, context) { 146 | return context.renderHtml(data.property("definitions")); 147 | } 148 | }, 149 | other: { 150 | title: "Other", 151 | renderHtml: function (data, context) { 152 | var result = ""; 153 | data.properties(function (key, subData) { 154 | if (knownSchemaKeys.indexOf(key) == -1) { 155 | result += '

' + key + ':

'; 156 | result += '
' + context.renderHtml(subData) + '
'; 157 | } 158 | }); 159 | return result; 160 | } 161 | } 162 | }, 163 | tabOrder: ["all", "number", "string", "object", "array", "definitions", "other"], 164 | renderHtml: function (data, context) { 165 | var result = '
'; 166 | if (context.uiState.expanded == undefined) { 167 | context.uiState.expanded = true; 168 | } 169 | if (!context.uiState.expanded) { 170 | result += context.actionHtml('show', 'expand'); 171 | } else { 172 | result += context.actionHtml('hide', 'collapse'); 173 | } 174 | result += '

'; 175 | if (data.readOnly() && !data.property("title").defined()) { 176 | result += '(untitled schema)'; 177 | } else { 178 | result += context.renderHtml(data.property("title")); 179 | } 180 | result += '

'; 181 | 182 | if (context.uiState.expanded) { 183 | result += '
'; 184 | 185 | result += '
'; 186 | result += context.renderHtml(data.property("description")); 187 | result += '
'; 188 | 189 | if (!data.readOnly()) { 190 | result += '
'; 191 | result += context.actionHtml("Replace with reference", "add-ref"); 192 | result += '
'; 193 | if (data.schemas().basicTypes().indexOf("boolean") != -1 || data.parentKey() == "additionalProperties" || data.parentKey() == "additionalItems") { 194 | result += '
'; 195 | result += context.actionHtml("Disallow", "replace-false"); 196 | result += '
'; 197 | } 198 | } 199 | 200 | if (!data.readOnly() || data.property("format").defined()) { 201 | result += '

Format:

'; 202 | result += '
' + context.renderHtml(data.property("format")) + '
'; 203 | } 204 | 205 | result += '

Type:

'; 206 | if (data.readOnly() && !data.property('type').defined()) { 207 | result += 'Any '; 208 | } 209 | result += context.renderHtml(data.property("type")) 210 | result += '
'; 211 | var types = data.property("type").value(); 212 | if (typeof types == "string") { 213 | types = [types]; 214 | } else if (types == null) { 215 | types = ["null", "boolean", "number", "string", "object", "array"]; 216 | } 217 | 218 | var tabs = { 219 | all: true, 220 | definitions: !data.readOnly() || data.property("definitions").defined() 221 | }; 222 | 223 | if (types.indexOf('object') != -1) { 224 | tabs.object = true; 225 | } 226 | if (types.indexOf('array') != -1) { 227 | tabs.array = true; 228 | } 229 | if (types.indexOf('number') != -1 || types.indexOf('integer') != -1) { 230 | tabs.number = true; 231 | } 232 | if (types.indexOf('string') != -1) { 233 | tabs.string = true; 234 | } 235 | 236 | // Tab bar 237 | result += '
'; 238 | var currentTab = null; 239 | if (context.uiState.currentTab == undefined) { 240 | context.uiState.currentTab = "all"; 241 | } 242 | data.properties(function (key, subData) { 243 | if (knownSchemaKeys.indexOf(key) == -1) { 244 | tabs.other = true; 245 | } 246 | }); 247 | for (var i = 0; i < this.tabOrder.length; i++) { 248 | var tabKey = this.tabOrder[i]; 249 | var tab = this.tabs[tabKey]; 250 | var subContext = context.subContext(); 251 | if (tabs[tabKey]) { 252 | if (context.uiState.currentTab == tabKey) { 253 | result += context.actionHtml('' + tab.title + '', 'select-tab', tabKey); 254 | currentTab = tab; 255 | } else { 256 | result += context.actionHtml('' + tab.title + '', 'select-tab', tabKey); 257 | } 258 | } 259 | } 260 | if (currentTab == null) { 261 | currentTab = this.tabs.all; 262 | } 263 | result += '
'; 264 | 265 | result += currentTab.renderHtml(data, context); 266 | 267 | result += '
'; 268 | } 269 | 270 | return result + "
"; 271 | }, 272 | action: function (context, actionName, tabKey) { 273 | if (actionName == "select-tab") { 274 | context.uiState.currentTab = tabKey; 275 | } else if (actionName == "add-ref") { 276 | context.data.property("$ref").setValue("#"); 277 | return false; 278 | } else if (actionName == "replace-false") { 279 | context.data.setValue(false); 280 | return false; 281 | } else if (actionName == "expand") { 282 | context.uiState.expanded = true; 283 | } else { 284 | context.uiState.expanded = false; 285 | } 286 | return true; 287 | }, 288 | filter: function (data, schemas) { 289 | return schemas.containsUrl('http://json-schema.org/schema') && data.basicType() == "object"; 290 | }, 291 | update: function (element, data, context, operation) { 292 | if (operation.hasPrefix(data.property("type").pointerPath())) { 293 | return true; 294 | } 295 | return this.defaultUpdate(element, data, context, operation); 296 | } 297 | }); 298 | 299 | Jsonary.render.register({ 300 | renderHtml: function (data, context) { 301 | var result = '
'; 302 | if (data.value()) { 303 | result += 'Anything'; 304 | } else { 305 | result += 'Not allowed'; 306 | } 307 | if (!data.readOnly()) { 308 | result += " (" + context.actionHtml("replace with schema", "replace-schema") + ")"; 309 | } 310 | return result + '
'; 311 | }, 312 | action: function (context, actionName) { 313 | var data = context.data; 314 | if (actionName == "replace-schema") { 315 | data.setValue({}); 316 | } 317 | }, 318 | filter: function (data, schemas) { 319 | return schemas.containsUrl('http://json-schema.org/schema') && data.basicType() == "boolean"; 320 | } 321 | }); 322 | 323 | Jsonary.render.register({ 324 | enhance: function (element, data, context) { 325 | var previewElement = document.createElement("div"); 326 | element.appendChild(previewElement); 327 | var fullLink = data.links("full")[0]; 328 | fullLink.follow(function (link, submissionData, request) { 329 | request.getData(function (data) { 330 | var result = '
'; 331 | if (!data.property("title").defined()) { 332 | result += '

(untitled schema)

'; 333 | } else { 334 | result += '

' + data.propertyValue("title") + '

'; 335 | } 336 | result += '
'; 337 | previewElement.innerHTML = result; 338 | }); 339 | return false; 340 | }); 341 | element = null; 342 | }, 343 | renderHtml: function (data, context) { 344 | var result = '
'; 345 | result += context.actionHtml("Reference", "follow"); 346 | result += ": " + context.renderHtml(data.property("$ref")); 347 | return result + "
"; 348 | }, 349 | action: function (context, actionName) { 350 | var data = context.data; 351 | if (actionName == "follow") { 352 | var fullLink = data.links('full')[0]; 353 | fullLink.follow(); 354 | } 355 | }, 356 | filter: function (data, schemas) { 357 | return schemas.containsUrl('http://json-schema.org/schema') && data.property("$ref").defined(); 358 | } 359 | }); 360 | 361 | Jsonary.addToCache("http://json-schema.org/schema", { 362 | "title": "JSON Schema", 363 | "type": "object", 364 | "properties": { 365 | "type": { 366 | "title": "Types", 367 | "oneOf": [ 368 | { 369 | "type": "array", 370 | "items": { 371 | "type": "string", 372 | "enum": ["null", "boolean", "integer", "number", "string", "object", "array"], 373 | }, 374 | "uniqueItems": true 375 | }, 376 | { 377 | "type": "string", 378 | "enum": ["null", "boolean", "integer", "number", "string", "object", "array"] 379 | } 380 | ] 381 | }, 382 | "title": { 383 | "title": "Schema title", 384 | "type": "string" 385 | }, 386 | "description": { 387 | "title": "Schema description", 388 | "type": "string" 389 | }, 390 | "oneOf": { 391 | "title": "One-Of", 392 | "description": "Instances must match exactly one of the schemas in this property", 393 | "type": "array", 394 | "items": {"$ref": "#"} 395 | }, 396 | "anyOf": { 397 | "title": "Any-Of", 398 | "description": "Instances must match at least one of the schemas in this property", 399 | "type": "array", 400 | "items": {"$ref": "#"} 401 | }, 402 | "allOf": { 403 | "title": "All-Of", 404 | "description": "Instances must match all of the schemas in this property", 405 | "type": "array", 406 | "items": {"$ref": "#"} 407 | }, 408 | "extends": { 409 | "title": "Extends (DEPRECATED)", 410 | "description": "Instances must match all of the schemas in this property", 411 | "type": "array", 412 | "items": {"$ref": "#"} 413 | }, 414 | "enum": { 415 | "title": "Enum values", 416 | "description": "If defined, then the value must be equal to one of the items in this array", 417 | "type": "array" 418 | }, 419 | "default": { 420 | "title": "Default value", 421 | "type": "any" 422 | }, 423 | "properties": { 424 | "title": "Object properties", 425 | "type": "object", 426 | "additionalProperties": {"$ref": "#"} 427 | }, 428 | "required": { 429 | "title": "Required properties", 430 | "description": "If the instance is an object, these properties must be present", 431 | "type": "array", 432 | "items": { 433 | "title": "Property name", 434 | "type": "string" 435 | } 436 | }, 437 | "dependencies": { 438 | "title": "Dependencies", 439 | "description": "If the instance is an object, and contains a property matching one of those here, then it must also follow the corresponding schema", 440 | "type": "object", 441 | "additionalProperties": {"$ref": "#"} 442 | }, 443 | "additionalProperties": { 444 | "oneOf": [ 445 | {"$ref": "#"}, 446 | {"type": "boolean"} 447 | ] 448 | }, 449 | "items": { 450 | "title": "Array items", 451 | "oneOf": [ 452 | {"$ref": "#"}, 453 | { 454 | "title": "Tuple type", 455 | "type": "array", 456 | "minItems": 1, 457 | "items": {"$ref": "#"} 458 | } 459 | ] 460 | }, 461 | "additionalItems": {"$ref": "#"}, 462 | "minItems": { 463 | "title": "Minimum array length", 464 | "type": "integer", 465 | "minimum": 0 466 | }, 467 | "maxItems": { 468 | "title": "Maximum array length", 469 | "type": "integer", 470 | "minimum": 0 471 | }, 472 | "uniqueItems": { 473 | "title": "Unique items", 474 | "description": "If set to true, and the value is an array, then no two items in the array should be equal.", 475 | "type": "boolean", 476 | "default": false 477 | }, 478 | "pattern": { 479 | "title": "Regular expression", 480 | "description": "An regular expression (ECMA 262) which the value must match if it's a string", 481 | "type": "string", 482 | "format": "regex" 483 | }, 484 | "minLength": { 485 | "title": "Minimum string length", 486 | "type": "integer", 487 | "minimum": 0, 488 | "default": 0 489 | }, 490 | "maxLength": { 491 | "title": "Maximum string length", 492 | "type": "integer", 493 | "minimum": 0 494 | }, 495 | "minimum": { 496 | "title": "Minimum value", 497 | "type": "number" 498 | }, 499 | "maximum": { 500 | "title": "Maximum value", 501 | "type": "number" 502 | }, 503 | "exclusiveMinimum": { 504 | "title": "Exclusive Minimum", 505 | "description": "If the value is a number and this is set to true, then the value cannot be equal to the value specified in \"minimum\"", 506 | "type": "boolean", 507 | "default": false 508 | }, 509 | "exclusiveMaximum": { 510 | "title": "Exclusive Maximum", 511 | "description": "If the value is a number and this is set to true, then the value cannot be equal to the value specified in \"maximum\"", 512 | "type": "boolean", 513 | "default": false 514 | }, 515 | "divisibleBy": { 516 | "title": "Divisible by", 517 | "description": "If the value is a number, then it must be an integer multiple of the value of this property", 518 | "type": "number", 519 | "minimum": 0, 520 | "exclusiveMinimum": true 521 | }, 522 | "$ref": { 523 | "title": "Reference URI", 524 | "description": "This contains the URI of a schema, which should be used to replace the containing schema.", 525 | "type": "string", 526 | "format": "uri" 527 | } 528 | }, 529 | "additionalProperties": {}, 530 | "links": [ 531 | { 532 | "href": "{+($ref)}", 533 | "rel": "full" 534 | } 535 | ] 536 | }, "http://json-schema.org/schema"); 537 | 538 | Jsonary.addToCache("http://json-schema.org/hyper-schema", { 539 | "allOf": [ 540 | {"$ref": "http://json-schema.org/schema"} 541 | ] 542 | }, "http://json-schema.org/hyper-schema"); 543 | })(Jsonary); 544 | -------------------------------------------------------------------------------- /wild-ideas/jsonary/plugins/jsonary.render.table.js: -------------------------------------------------------------------------------- 1 | (function (Jsonary) { 2 | 3 | function TableRenderer (config) { 4 | if (!(this instanceof TableRenderer)) { 5 | return new TableRenderer(config); 6 | } 7 | var thisRenderer = this; 8 | 9 | config = config || {}; 10 | this.config = config; 11 | 12 | for (var key in TableRenderer.defaults) { 13 | if (!config[key]) { 14 | if (typeof TableRenderer.defaults[key] == "function") { 15 | config[key] = TableRenderer.defaults[key]; 16 | } else { 17 | config[key] = JSON.parse(JSON.stringify(TableRenderer.defaults[key])); 18 | } 19 | } 20 | } 21 | 22 | for (var i = 0; i < config.columns.length; i++) { 23 | var columnPath = config.columns[i]; 24 | config.cellRenderHtml[key] = config.cellRenderHtml[key] || config.defaultCellRenderHtml; 25 | config.titleHtml[key] = config.titleHtml[key] || config.defaultTitleHtml; 26 | } 27 | 28 | config.rowRenderHtml = this.wrapRowFunction(config, config.rowRenderHtml); 29 | for (var key in config.cellRenderHtml) { 30 | config.cellRenderHtml[key] = this.wrapCellFunction(config, config.cellRenderHtml[key], key); 31 | } 32 | for (var key in config.titleHtml) { 33 | config.titleHtml[key] = this.wrapTitleFunction(config, config.titleHtml[key], key); 34 | } 35 | 36 | if (config.filter) { 37 | this.filter = function (data, schemas) { 38 | return config.filter(data, schemas); 39 | }; 40 | } 41 | 42 | this.addColumn = function (key, title, renderHtml) { 43 | config.columns.push(key); 44 | if (typeof title == 'function') { 45 | config.titleHtml[key] = thisRenderer.wrapTitleFunction(config, title, key); 46 | } else { 47 | if (title != undefined) { 48 | config.titles[key] = title; 49 | } 50 | config.titleHtml[key] = thisRenderer.wrapTitleFunction(config, config.defaultTitleHtml, key); 51 | } 52 | renderHtml = renderHtml || config.defaultCellRenderHtml; 53 | config.cellRenderHtml[key] = thisRenderer.wrapCellFunction(config, renderHtml, key); 54 | return this; 55 | } 56 | 57 | this.component = config.component; 58 | }; 59 | TableRenderer.prototype = { 60 | wrapRowFunction: function (functionThis, original) { 61 | var thisRenderer = this; 62 | return function (rowData, context) { 63 | var rowContext = thisRenderer.rowContext(rowData, context); 64 | return original.call(functionThis, rowData, rowContext); 65 | }; 66 | }, 67 | wrapTitleFunction: function (functionThis, original, columnKey) { 68 | var thisRenderer = this; 69 | return function (cellData, context) { 70 | // var titleContext = context.subContext('title' + columnKey); 71 | // titleContext.columnPath = titleContext; 72 | var titleContext = context; 73 | return original.call(functionThis, cellData, titleContext, columnKey); 74 | } 75 | }, 76 | wrapCellFunction: function (functionThis, original, columnKey) { 77 | var thisRenderer = this; 78 | return function (cellData, context) { 79 | var cellContext = thisRenderer.cellContext(cellData, context, columnKey); 80 | return original.call(functionThis, cellData, cellContext); 81 | } 82 | }, 83 | action: function (context, actionName) { 84 | if (context.cellData) { 85 | var columnPath = context.columnPath; 86 | var cellAction = this.config.cellAction[columnPath]; 87 | var newArgs = [context.cellData]; 88 | while (newArgs.length <= arguments.length) { 89 | newArgs.push(arguments[newArgs.length - 1]); 90 | } 91 | return cellAction.apply(this.config, newArgs); 92 | } else if (context.rowData) { 93 | var rowAction = this.config.rowAction; 94 | var newArgs = [context.rowData]; 95 | while (newArgs.length <= arguments.length) { 96 | newArgs.push(arguments[newArgs.length - 1]); 97 | } 98 | return rowAction.apply(this.config, newArgs); 99 | } 100 | var newArgs = [context.data]; 101 | while (newArgs.length <= arguments.length) { 102 | newArgs.push(arguments[newArgs.length - 1]); 103 | } 104 | return this.config.action.apply(this.config, newArgs); 105 | }, 106 | rowContext: function (data, context) { 107 | var subContext = context.subContext(data); 108 | subContext.rowData = data; 109 | return subContext; 110 | }, 111 | cellContext: function (data, context, columnPath) { 112 | var subContext = context.subContext('col' + columnPath); 113 | subContext.columnPath = columnPath; 114 | subContext.cellData = data; 115 | return subContext; 116 | }, 117 | renderHtml: function (data, context) { 118 | return this.config.tableRenderHtml(data, context); 119 | }, 120 | rowRenderHtml: function (data, context) { 121 | var config = this.config; 122 | return config.rowRenderHtml(data, context); 123 | }, 124 | enhance: function (element, data, context) { 125 | if (this.config.enhance) { 126 | return this.config.enhance(element, data, context); 127 | } else if (this.config.render) { 128 | return this.config.render(element, data, context); 129 | } 130 | }, 131 | register: function(filterFunction) { 132 | if (filterFunction) { 133 | this.filter = filterFunction; 134 | } 135 | return Jsonary.render.register(this); 136 | } 137 | }; 138 | TableRenderer.defaults = { 139 | columns: [], 140 | titles: {}, 141 | titleHtml: {}, 142 | defaultTitleHtml: function (data, context, columnPath) { 143 | return '' + Jsonary.escapeHtml(this.titles[columnPath] != undefined ? this.titles[columnPath] : columnPath) + ''; 144 | }, 145 | cellRenderHtml: {}, 146 | defaultCellRenderHtml: function (cellData, context, columnPath) { 147 | return '' + context.renderHtml(cellData) + ''; 148 | }, 149 | cellAction: {}, 150 | rowRenderHtml: function (rowData, context) { 151 | var result = ""; 152 | for (var i = 0; i < this.columns.length; i++) { 153 | var columnPath = this.columns[i]; 154 | var cellData = (columnPath == "" || columnPath.charAt(0) == "/") ? rowData.subPath(columnPath) : rowData; 155 | var cellRenderHtml = this.cellRenderHtml[columnPath]; 156 | result += this.cellRenderHtml[columnPath](cellData, context); 157 | } 158 | result += ''; 159 | return result; 160 | }, 161 | rowAction: function (data, context, actionName) { 162 | throw new Error("Unknown row action: " + actionName); 163 | }, 164 | tableRenderHtml: function (data, context) { 165 | var result = ''; 166 | result += ''; 167 | result += this.tableHeadRenderHtml(data, context); 168 | result += this.tableBodyRenderHtml(data, context); 169 | result += '
'; 170 | return result; 171 | }, 172 | tableHeadRenderHtml: function (data, context) { 173 | var result = ''; 174 | for (var i = 0; i < this.columns.length; i++) { 175 | var columnPath = this.columns[i]; 176 | result += this.titleHtml[columnPath](data, context); 177 | } 178 | return result + ''; 179 | }, 180 | rowOrder: function (data, context) { 181 | var result = []; 182 | var length = data.length; 183 | while (result.length < length) { 184 | result[result.length] = result.length; 185 | } 186 | return result; 187 | }, 188 | tableBodyRenderHtml: function (data, context) { 189 | var config = this.config; 190 | var result = ''; 191 | 192 | var rowOrder = this.rowOrder(data, context); 193 | for (var i = 0; i < rowOrder.length; i++) { 194 | var rowData = data.item(currentPage[i]); 195 | result += this.rowRenderHtml(rowData, context); 196 | } 197 | 198 | if (!data.readOnly()) { 199 | if (data.schemas().maxItems() == null || data.schemas().maxItems() > data.length()) { 200 | result += ''; 201 | result += context.actionHtml('+ add', 'add'); 202 | result += ''; 203 | } 204 | } 205 | return result + ''; 206 | }, 207 | action: function (data, context, actionName) { 208 | if (actionName == "add") { 209 | var index = data.length(); 210 | var schemas = data.schemas().indexSchemas(index); 211 | schemas.createValue(function (value) { 212 | data.push(value); 213 | }); 214 | return false; 215 | } 216 | } 217 | }; 218 | 219 | /** Fancy tables with sorting and links **/ 220 | function FancyTableRenderer(config) { 221 | if (!(this instanceof FancyTableRenderer)) { 222 | return new FancyTableRenderer(config); 223 | } 224 | config = config || {}; 225 | 226 | for (var key in FancyTableRenderer.defaults) { 227 | if (!config[key]) { 228 | if (typeof FancyTableRenderer.defaults[key] == "function") { 229 | config[key] = FancyTableRenderer.defaults[key]; 230 | } else { 231 | config[key] = JSON.parse(JSON.stringify(FancyTableRenderer.defaults[key])); 232 | } 233 | } 234 | } 235 | 236 | for (var key in config.sort) { 237 | if (typeof config.sort[key] !== 'function') { 238 | config.sort[key] = config.defaultSort; 239 | } 240 | } 241 | 242 | TableRenderer.call(this, config); 243 | 244 | var prevAddColumn = this.addColumn; 245 | this.addColumn = function (key, title, renderHtml, sorting) { 246 | if (sorting) { 247 | config.sort[key] = (typeof sorting == 'function') ? sorting : config.defaultSort; 248 | } 249 | return prevAddColumn.call(this, key, title, renderHtml); 250 | }; 251 | } 252 | FancyTableRenderer.prototype = Object.create(TableRenderer.prototype); 253 | FancyTableRenderer.prototype.addLinkColumn = function (linkRel, title, linkHtml, activeHtml, isConfirm) { 254 | if (typeof linkRel == "string") { 255 | var columnName = "link$" + linkRel; 256 | 257 | this.addColumn(columnName, title, function (data, context) { 258 | if (!context.data.readOnly()) { 259 | return ''; 260 | } 261 | var result = ''; 262 | if (!context.parent.uiState.linkRel) { 263 | var link = data.links(linkRel)[0]; 264 | if (link) { 265 | result += context.parent.actionHtml(linkHtml, 'link', linkRel); 266 | } 267 | } else if (activeHtml) { 268 | var activeLink = data.links(context.parent.uiState.linkRel)[context.parent.uiState.linkIndex || 0]; 269 | if (activeLink.rel == linkRel) { 270 | if (isConfirm) { 271 | result += context.parent.actionHtml(activeHtml, 'link-confirm', context.parent.uiState.linkRel, context.parent.uiState.linkIndex); 272 | } else { 273 | result += context.parent.actionHtml(activeHtml, 'link-cancel'); 274 | } 275 | } 276 | } 277 | return result + ''; 278 | }); 279 | } else { 280 | var linkDefinition = linkRel; 281 | linkRel = linkDefinition.rel(); 282 | var columnName = "link$" + linkRel + "$" + linkHtml; 283 | this.addColumn(columnName, title, function (data, context) { 284 | var result = ''; 285 | if (!context.parent.uiState.linkRel) { 286 | var links = data.links(linkRel); 287 | for (var i = 0; i < links.length; i++) { 288 | var link = links[i]; 289 | if (link.definition = linkDefinition) { 290 | result += context.parent.actionHtml(linkHtml, 'link', linkRel, i); 291 | } 292 | } 293 | } else if (activeHtml) { 294 | var activeLink = data.links(context.parent.uiState.linkRel)[context.parent.uiState.linkIndex || 0]; 295 | if (activeLink.definition == linkDefinition) { 296 | if (isConfirm) { 297 | result += context.parent.actionHtml(activeHtml, 'link-confirm', context.parent.uiState.linkRel, context.parent.uiState.linkIndex); 298 | } else { 299 | result += context.parent.actionHtml(activeHtml, 'link-cancel'); 300 | } 301 | } 302 | } 303 | return result + ''; 304 | }); 305 | } 306 | return this; 307 | }; 308 | 309 | FancyTableRenderer.defaults = { 310 | sort: {}, 311 | defaultSort: function (a, b) { 312 | if (a == null) { 313 | return (b == null) ? 0 : -1; 314 | } else if (b == null || a > b) { 315 | return 1; 316 | } else if (a < b) { 317 | return -1; 318 | } 319 | return 0; 320 | }, 321 | rowOrder: function (data, context) { 322 | var thisConfig = this; 323 | var sortFunctions = []; 324 | context.uiState.sort = context.uiState.sort || []; 325 | 326 | function addSortFunction(sortIndex, sortKey) { 327 | var direction = sortKey.split('/')[0]; 328 | var path = sortKey.substring(direction.length); 329 | var multiplier = (direction == "desc") ? -1 : 1; 330 | sortFunctions.push(function (a, b) { 331 | var valueA = a.get(path); 332 | var valueB = b.get(path); 333 | var comparison = thisConfig.sort[path] ? thisConfig.sort[path](valueA, valueB) : thisConfig.defaultSort(valueA, valueB); 334 | return multiplier*comparison; 335 | }); 336 | } 337 | for (var i = 0; i < context.uiState.sort.length; i++) { 338 | addSortFunction(i, context.uiState.sort[i]); 339 | } 340 | var indices = []; 341 | var length = data.length(); 342 | while (indices.length < length) { 343 | indices[indices.length] = indices.length; 344 | } 345 | var maxSortIndex = -1; 346 | indices.sort(function (a, b) { 347 | for (var i = 0; i < sortFunctions.length; i++) { 348 | var comparison = sortFunctions[i](data.item(a), data.item(b)); 349 | if (comparison != 0) { 350 | maxSortIndex = Math.max(maxSortIndex, i); 351 | return comparison; 352 | } 353 | } 354 | maxSortIndex = sortFunctions.length; 355 | return a - b; 356 | }); 357 | // Trim sort conditions list, for smaller UI state 358 | context.uiState.sort = context.uiState.sort.slice(0, maxSortIndex + 1); 359 | return indices; 360 | }, 361 | rowsPerPage: null, 362 | pages: function (rowOrder) { 363 | if (this.rowsPerPage == null) { 364 | return [rowOrder]; 365 | } 366 | var pages = []; 367 | while (rowOrder.length) { 368 | pages.push(rowOrder.splice(0, this.rowsPerPage)); 369 | } 370 | return pages; 371 | }, 372 | tableHeadRenderHtml: function (data, context) { 373 | var result = ''; 374 | var rowOrder = this.rowOrder(data, context); 375 | var pages = this.pages(rowOrder); 376 | if (pages.length > 1) { 377 | var page = context.uiState.page || 0; 378 | result += ''; 379 | if (page > 0) { 380 | result += context.actionHtml('<<', 'page', 0); 381 | result += context.actionHtml('<', 'page', page - 1); 382 | } else { 383 | result += '<<'; 384 | result += '<'; 385 | } 386 | result += 'page '; 395 | if (page < pages.length - 1) { 396 | result += context.actionHtml('>', 'page', page + 1); 397 | result += context.actionHtml('>>', 'page', pages.length - 1); 398 | } else { 399 | result += '>'; 400 | result += '>>'; 401 | } 402 | result += ''; 403 | } 404 | result += ''; 405 | for (var i = 0; i < this.columns.length; i++) { 406 | var columnKey = this.columns[i]; 407 | result += this.titleHtml[columnKey](data, context); 408 | } 409 | result += ''; 410 | return result + ''; 411 | }, 412 | tableBodyRenderHtml: function (data, context) { 413 | var config = this.config; 414 | var result = ''; 415 | var rowOrder = this.rowOrder(data, context); 416 | 417 | var pages = this.pages(rowOrder); 418 | var page = context.uiState.page || 0; 419 | var pageRows = pages[page]; 420 | if (!pageRows) { 421 | pageRows = pages[0] || []; 422 | context.uiState.page = 0; 423 | } 424 | for (var i = 0; i < pageRows.length; i++) { 425 | var rowData = data.item(pageRows[i]); 426 | result += this.rowRenderHtml(rowData, context); 427 | } 428 | if (page == pages.length - 1 && !data.readOnly()) { 429 | if (data.schemas().maxItems() == null || data.schemas().maxItems() > data.length()) { 430 | result += ''; 431 | result += context.actionHtml('+ add', 'add'); 432 | result += ''; 433 | } 434 | } 435 | return result + ''; 436 | }, 437 | action: function (data, context, actionName, arg1) { 438 | if (actionName == "sort") { 439 | delete context.uiState.page; 440 | var columnKey = arg1; 441 | context.uiState.sort = context.uiState.sort || []; 442 | if (context.uiState.sort[0] == "asc" + columnKey) { 443 | context.uiState.sort[0] = "desc" + arg1; 444 | } else { 445 | if (context.uiState.sort.indexOf("desc" + columnKey) != -1) { 446 | context.uiState.sort.splice(context.uiState.sort.indexOf("desc" + columnKey), 1); 447 | } else if (context.uiState.sort.indexOf("asc" + columnKey) != -1) { 448 | context.uiState.sort.splice(context.uiState.sort.indexOf("asc" + columnKey), 1); 449 | } 450 | context.uiState.sort.unshift("asc" + arg1); 451 | } 452 | return true; 453 | } else if (actionName == "page") { 454 | context.uiState.page = parseInt(arg1); 455 | return true; 456 | } 457 | return TableRenderer.defaults.action.apply(this, arguments); 458 | }, 459 | defaultTitleHtml: function (data, context, columnKey) { 460 | if (data.readOnly() && columnKey.charAt(0) == "/" && this.sort[columnKey]) { 461 | var result = ''; 462 | context.uiState.sort = context.uiState.sort || []; 463 | result += context.actionHtml(Jsonary.escapeHtml(this.titles[columnKey]), 'sort', columnKey); 464 | if (context.uiState.sort[0] == "asc" + columnKey) { 465 | result += ' up' 466 | } else if (context.uiState.sort[0] == "desc" + columnKey) { 467 | result += ' down' 468 | } 469 | return result + '' 470 | } 471 | return TableRenderer.defaults.defaultTitleHtml.call(this, data, context, columnKey); 472 | }, 473 | rowRenderHtml: function (data, context) { 474 | var result = ''; 475 | if (context.uiState.expand) { 476 | result += TableRenderer.defaults.rowRenderHtml.call(this, data, context); 477 | result += ''; 478 | if (context.uiState.expand === true) { 479 | result += context.renderHtml(data); 480 | } else { 481 | result += context.renderHtml(context.uiState.expand); 482 | } 483 | result += ''; 484 | } else if (context.uiState.linkRel) { 485 | var link = data.links(context.uiState.linkRel)[context.uiState.linkIndex || 0]; 486 | if (context.uiState.linkData) { 487 | if (link.rel == "edit" && link.submissionSchemas.length == 0) { 488 | result += TableRenderer.defaults.rowRenderHtml.call(this, context.uiState.linkData, context); 489 | } else { 490 | result += TableRenderer.defaults.rowRenderHtml.call(this, data, context); 491 | result += ''; 492 | result += '
' + Jsonary.escapeHtml(link.title || link.rel) + '
'; 493 | result += '
'; 494 | result += context.actionHtml('confirm', 'link-confirm', context.uiState.linkRel, context.uiState.linkIndex); 495 | result += context.actionHtml(' cancel', 'link-cancel'); 496 | result += '
'; 497 | result += context.renderHtml(context.uiState.linkData); 498 | result += ''; 499 | } 500 | } else { 501 | result += TableRenderer.defaults.rowRenderHtml.call(this, data, context); 502 | result += ''; 503 | result += '
' + Jsonary.escapeHtml(link.title || link.rel) + '
'; 504 | result += '
'; 505 | result += context.actionHtml('confirm', 'link-confirm', context.uiState.linkRel, context.uiState.linkIndex); 506 | result += context.actionHtml(' cancel', 'link-cancel'); 507 | result += '
'; 508 | result += ''; 509 | } 510 | } else { 511 | result += TableRenderer.defaults.rowRenderHtml.call(this, data, context); 512 | } 513 | return result; 514 | }, 515 | rowAction: function (data, context, actionName, arg1, arg2) { 516 | if (actionName == "expand") { 517 | if (context.uiState.expand) { 518 | delete context.uiState.expand; 519 | } else { 520 | context.uiState.expand = true; 521 | } 522 | return true; 523 | } else if (actionName == "link") { 524 | var linkRel = arg1, linkIndex = arg2 525 | var link = data.links(linkRel)[linkIndex || 0]; 526 | if (link.submissionSchemas.length) { 527 | context.uiState.linkRel = linkRel; 528 | context.uiState.linkIndex = linkIndex; 529 | var linkData = Jsonary.create(); 530 | linkData.addSchema(link.submissionSchemas); 531 | context.uiState.linkData = linkData; 532 | link.submissionSchemas.createValue(function (value) { 533 | linkData.setValue(value); 534 | }); 535 | delete context.uiState.expand; 536 | } else if (link.rel == "edit") { 537 | context.uiState.linkRel = linkRel; 538 | context.uiState.linkIndex = linkIndex; 539 | context.uiState.linkData = data.editableCopy(); 540 | delete context.uiState.expand; 541 | } else if (link.method != "GET") { 542 | context.uiState.linkRel = linkRel; 543 | context.uiState.linkIndex = linkIndex; 544 | delete context.uiState.linkData; 545 | delete context.uiState.expand; 546 | } else { 547 | var targetExpand = (link.rel == "self") ? true : link.href; 548 | if (context.uiState.expand == targetExpand) { 549 | delete context.uiState.expand; 550 | } else { 551 | context.uiState.expand = targetExpand; 552 | } 553 | } 554 | return true; 555 | } else if (actionName == "link-confirm") { 556 | var linkRel = arg1, linkIndex = arg2 557 | var link = data.links(linkRel)[linkIndex || 0]; 558 | if (link) { 559 | link.follow(context.uiState.linkData, this.linkHandler); 560 | } 561 | delete context.uiState.linkRel; 562 | delete context.uiState.linkIndex; 563 | delete context.uiState.linkData; 564 | delete context.uiState.expand; 565 | return true; 566 | } else if (actionName == "link-cancel") { 567 | delete context.uiState.linkRel; 568 | delete context.uiState.linkIndex; 569 | delete context.uiState.linkData; 570 | delete context.uiState.expand; 571 | return true; 572 | } 573 | return TableRenderer.defaults.rowAction.apply(this, arguments); 574 | }, 575 | linkHandler: function () {} 576 | }; 577 | 578 | Jsonary.plugins = Jsonary.plugins || {}; 579 | Jsonary.plugins.TableRenderer = TableRenderer; 580 | Jsonary.plugins.FancyTableRenderer = FancyTableRenderer; 581 | })(Jsonary); -------------------------------------------------------------------------------- /wild-ideas/jsonary/renderers/basic.jsonary.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function escapeHtml(text) { 3 | return text.replace(/&/g, "&").replace(//g, ">").replace(/'/g, "'").replace(/"/g, """); 4 | } 5 | if (window.escapeHtml == undefined) { 6 | window.escapeHtml = escapeHtml; 7 | } 8 | 9 | Jsonary.render.register({ 10 | component: Jsonary.render.Components.ADD_REMOVE, 11 | renderHtml: function (data, context) { 12 | if (!data.defined()) { 13 | context.uiState.undefined = true; 14 | return context.actionHtml('+ create', "create"); 15 | } 16 | delete context.uiState.undefined; 17 | var showDelete = false; 18 | if (data.parent() != null) { 19 | var parent = data.parent(); 20 | if (parent.basicType() == "object") { 21 | var required = parent.schemas().requiredProperties(); 22 | var minProperties = parent.schemas().minProperties(); 23 | showDelete = required.indexOf(data.parentKey()) == -1 && parent.keys().length > minProperties; 24 | } else if (parent.basicType() == "array") { 25 | var tupleTypingLength = parent.schemas().tupleTypingLength(); 26 | var minItems = parent.schemas().minItems(); 27 | var index = parseInt(data.parentKey()); 28 | if ((index >= tupleTypingLength || index == parent.length() - 1) 29 | && parent.length() > minItems) { 30 | showDelete = true; 31 | } 32 | } 33 | } 34 | var result = ""; 35 | if (showDelete) { 36 | result += "
"; 37 | result += context.actionHtml("X", "remove") + " "; 38 | result += context.renderHtml(data); 39 | result += '
'; 40 | } else { 41 | result += context.renderHtml(data); 42 | } 43 | return result; 44 | }, 45 | action: function (context, actionName) { 46 | if (actionName == "create") { 47 | var data = context.data; 48 | var parent = data.parent(); 49 | var finalComponent = data.parentKey(); 50 | if (parent != undefined) { 51 | var parentSchemas = parent.schemas(); 52 | if (parent.basicType() == "array") { 53 | parentSchemas.createValueForIndex(finalComponent, function (newValue) { 54 | parent.index(finalComponent).setValue(newValue); 55 | }); 56 | } else { 57 | if (parent.basicType() != "object") { 58 | parent.setValue({}); 59 | } 60 | parentSchemas.createValueForProperty(finalComponent, function (newValue) { 61 | parent.property(finalComponent).setValue(newValue); 62 | }); 63 | } 64 | } else { 65 | data.schemas().createValue(function (newValue) { 66 | data.setValue(newValue); 67 | }); 68 | } 69 | } else if (actionName == "remove") { 70 | context.data.remove(); 71 | } else { 72 | alert("Unkown action: " + actionName); 73 | } 74 | }, 75 | update: function (element, data, context, operation) { 76 | return context.uiState.undefined; 77 | }, 78 | filter: function (data) { 79 | return !data.readOnly(); 80 | } 81 | }); 82 | 83 | Jsonary.render.register({ 84 | component: Jsonary.render.Components.TYPE_SELECTOR, 85 | renderHtml: function (data, context) { 86 | var result = ""; 87 | var basicTypes = data.schemas().basicTypes(); 88 | var enums = data.schemas().enumValues(); 89 | if (context.uiState.dialogOpen) { 90 | result += '
'; 91 | result += context.actionHtml('close', "closeDialog"); 92 | if (basicTypes.length > 1) { 93 | result += '
Select basic type:
    '; 94 | for (var i = 0; i < basicTypes.length; i++) { 95 | if (basicTypes[i] == "integer" && basicTypes.indexOf("number") != -1) { 96 | continue; 97 | } 98 | if (basicTypes[i] == data.basicType() || basicTypes[i] == "number" && data.basicType() == "integer") { 99 | result += '
  • ' + basicTypes[i]; 100 | } else { 101 | result += '
  • ' + context.actionHtml(basicTypes[i], 'select-basic-type', basicTypes[i]); 102 | } 103 | } 104 | result += '
'; 105 | } 106 | result += '
'; 107 | } 108 | if (basicTypes.length > 1 && enums == null) { 109 | result += context.actionHtml("T", "openDialog") + " "; 110 | } 111 | result += context.renderHtml(data); 112 | return result; 113 | }, 114 | action: function (context, actionName, basicType) { 115 | if (actionName == "closeDialog") { 116 | context.uiState.dialogOpen = false; 117 | return true; 118 | } else if (actionName == "openDialog") { 119 | context.uiState.dialogOpen = true; 120 | return true; 121 | } else if (actionName == "select-basic-type") { 122 | context.uiState.dialogOpen = false; 123 | var schemas = context.data.schemas().concat([Jsonary.createSchema({type: basicType})]); 124 | schemas.createValue(function (newValue) { 125 | context.data.setValue(newValue); 126 | }); 127 | return true; 128 | } else { 129 | alert("Unkown action: " + actionName); 130 | } 131 | }, 132 | update: function (element, data, context, operation) { 133 | return false; 134 | }, 135 | filter: function (data) { 136 | return !data.readOnly(); 137 | } 138 | }); 139 | 140 | // Display schema switcher 141 | Jsonary.render.Components.add("SCHEMA_SWITCHER"); 142 | Jsonary.render.register({ 143 | component: Jsonary.render.Components.SCHEMA_SWITCHER, 144 | renderHtml: function (data, context) { 145 | var result = ""; 146 | var fixedSchemas = data.schemas().fixed(); 147 | context.uiState.xorSelected = []; 148 | context.uiState.orSelected = []; 149 | 150 | var singleOption = false; 151 | if (fixedSchemas.length < data.schemas().length) { 152 | var orSchemas = fixedSchemas.orSchemas(); 153 | if (orSchemas.length == 0) { 154 | var xorSchemas = fixedSchemas.xorSchemas(); 155 | singleOption = true; 156 | } 157 | } 158 | if (singleOption) { 159 | for (var i = 0; i < xorSchemas.length; i++) { 160 | var options = xorSchemas[i]; 161 | var inputName = context.inputNameForAction('selectXorSchema', i); 162 | result += ''; 174 | } 175 | } 176 | 177 | if (context.uiState.dialogOpen) { 178 | result += '
'; 179 | result += context.actionHtml('close', "closeDialog"); 180 | xorSchemas = xorSchemas || fixedSchemas.xorSchemas(); 181 | for (var i = 0; i < xorSchemas.length; i++) { 182 | var options = xorSchemas[i]; 183 | var inputName = context.inputNameForAction('selectXorSchema', i); 184 | result += '
'; 196 | } 197 | orSchemas = orSchemas || fixedSchemas.orSchemas(); 198 | for (var i = 0; i < orSchemas.length; i++) { 199 | var options = orSchemas[i]; 200 | var inputName = context.inputNameForAction('selectOrSchema', i); 201 | result += '
'; 216 | } 217 | result += '
'; 218 | } 219 | if (!singleOption && fixedSchemas.length < data.schemas().length) { 220 | result += context.actionHtml("S", "openDialog") + " "; 221 | } 222 | result += context.renderHtml(data); 223 | return result; 224 | }, 225 | createValue: function (context) { 226 | var data = context.data; 227 | var newSchemas = context.data.schemas().fixed(); 228 | var xorSchemas = context.data.schemas().fixed().xorSchemas(); 229 | for (var i = 0; i < xorSchemas.length; i++) { 230 | newSchemas = newSchemas.concat([xorSchemas[i][context.uiState.xorSelected[i]]]); 231 | } 232 | var orSchemas = context.data.schemas().fixed().orSchemas(); 233 | for (var i = 0; i < orSchemas.length; i++) { 234 | var options = orSchemas[i]; 235 | for (var j = 0; j < options.length; j++) { 236 | if (context.uiState.orSelected[i][j]) { 237 | newSchemas = newSchemas.concat([options[j]]); 238 | } 239 | } 240 | } 241 | newSchemas.getFull(function (sl) {newSchemas = sl;}); 242 | data.setValue(newSchemas.createValue()); 243 | }, 244 | action: function (context, actionName, value, arg1) { 245 | if (actionName == "closeDialog") { 246 | context.uiState.dialogOpen = false; 247 | return true; 248 | } else if (actionName == "openDialog") { 249 | context.uiState.dialogOpen = true; 250 | return true; 251 | } else if (actionName == "selectXorSchema") { 252 | context.uiState.xorSelected[arg1] = value; 253 | this.createValue(context); 254 | return true; 255 | } else if (actionName == "selectOrSchema") { 256 | context.uiState.orSelected[arg1] = []; 257 | for (var i = 0; i < value.length; i++) { 258 | context.uiState.orSelected[arg1][value[i]] = true; 259 | } 260 | this.createValue(context); 261 | return true; 262 | } else { 263 | alert("Unkown action: " + actionName); 264 | } 265 | }, 266 | update: function (element, data, context, operation) { 267 | return false; 268 | }, 269 | filter: function (data) { 270 | return !data.readOnly(); 271 | } 272 | }); 273 | 274 | // Display raw JSON 275 | Jsonary.render.register({ 276 | renderHtml: function (data, context) { 277 | if (!data.defined()) { 278 | return ""; 279 | } 280 | return '' + escapeHtml(JSON.stringify(data.value())) + ''; 281 | }, 282 | filter: function (data) { 283 | return true; 284 | } 285 | }); 286 | 287 | function updateTextAreaSize(textarea) { 288 | var lines = textarea.value.split("\n"); 289 | var maxWidth = 4; 290 | for (var i = 0; i < lines.length; i++) { 291 | if (maxWidth < lines[i].length) { 292 | maxWidth = lines[i].length; 293 | } 294 | } 295 | textarea.setAttribute("cols", maxWidth + 1); 296 | textarea.setAttribute("rows", lines.length); 297 | } 298 | 299 | // Display/edit objects 300 | Jsonary.render.register({ 301 | renderHtml: function (data, context) { 302 | var uiState = context.uiState; 303 | var result = '{'; 304 | data.properties(function (key, subData) { 305 | result += ''; 306 | result += ''; 307 | result += ''; 308 | result += ''; 309 | }); 310 | result += '
' + escapeHtml(key) + ':
' + context.renderHtml(subData) + '
'; 311 | if (!data.readOnly()) { 312 | var schemas = data.schemas(); 313 | var maxProperties = schemas.maxProperties(); 314 | if (maxProperties == null || maxProperties > data.keys().length) { 315 | var addLinkHtml = ""; 316 | var definedProperties = schemas.definedProperties(); 317 | var keyFunction = function (index, key) { 318 | var addHtml = '' + escapeHtml(key) + ''; 319 | addLinkHtml += context.actionHtml(addHtml, "add-named", key); 320 | }; 321 | for (var i = 0; i < definedProperties.length; i++) { 322 | if (!data.property(definedProperties[i]).defined()) { 323 | keyFunction(i, definedProperties[i]); 324 | } 325 | } 326 | if (schemas.allowedAdditionalProperties()) { 327 | var newHtml = '+ new'; 328 | addLinkHtml += context.actionHtml(newHtml, "add-new"); 329 | } 330 | if (addLinkHtml != "") { 331 | result += 'add: ' + addLinkHtml + ''; 332 | } 333 | } 334 | } 335 | return result + "}"; 336 | }, 337 | action: function (context, actionName, arg1) { 338 | var data = context.data; 339 | if (actionName == "add-named") { 340 | var key = arg1; 341 | data.schemas().createValueForProperty(key, function (newValue) { 342 | data.property(key).setValue(newValue); 343 | }); 344 | } else if (actionName == "add-new") { 345 | var key = window.prompt("New key:", "key"); 346 | if (key != null && !data.property(key).defined()) { 347 | data.schemas().createValueForProperty(key, function (newValue) { 348 | data.property(key).setValue(newValue); 349 | }); 350 | } 351 | } 352 | }, 353 | filter: function (data) { 354 | return data.basicType() == "object"; 355 | } 356 | }); 357 | 358 | // Display/edit arrays 359 | Jsonary.render.register({ 360 | renderHtml: function (data, context) { 361 | var tupleTypingLength = data.schemas().tupleTypingLength(); 362 | var maxItems = data.schemas().maxItems(); 363 | var result = "["; 364 | data.indices(function (index, subData) { 365 | result += '
'; 366 | result += '' + context.renderHtml(subData) + ''; 367 | result += '
'; 368 | }); 369 | if (!data.readOnly()) { 370 | if (maxItems == null || data.length() < maxItems) { 371 | var addHtml = '+ add'; 372 | result += context.actionHtml(addHtml, "add"); 373 | } 374 | } 375 | return result + "]"; 376 | }, 377 | action: function (context, actionName) { 378 | var data = context.data; 379 | if (actionName == "add") { 380 | var index = data.length(); 381 | data.schemas().createValueForIndex(index, function (newValue) { 382 | data.index(index).setValue(newValue); 383 | }); 384 | } 385 | }, 386 | filter: function (data) { 387 | return data.basicType() == "array"; 388 | } 389 | }); 390 | 391 | // Display string 392 | Jsonary.render.register({ 393 | renderHtml: function (data, context) { 394 | return '' + escapeHtml(data.value()) + ''; 395 | }, 396 | filter: function (data) { 397 | return data.basicType() == "string" && data.readOnly(); 398 | } 399 | }); 400 | 401 | // Display string 402 | Jsonary.render.register({ 403 | renderHtml: function (data, context) { 404 | var date = new Date(data.value()); 405 | return '' + date.toLocaleString() + ''; 406 | }, 407 | filter: function (data, schemas) { 408 | return data.basicType() == "string" && data.readOnly() && schemas.formats().indexOf("date-time") != -1; 409 | } 410 | }); 411 | 412 | function copyTextStyle(source, target) { 413 | var style = getComputedStyle(source, null); 414 | for (var key in style) { 415 | if (key.substring(0, 4) == "font" || key.substring(0, 4) == "text") { 416 | target.style[key] = style[key]; 417 | } 418 | } 419 | } 420 | function updateTextareaSize(textarea, sizeMatchBox, suffix) { 421 | sizeMatchBox.innerHTML = ""; 422 | sizeMatchBox.appendChild(document.createTextNode(textarea.value + suffix)); 423 | var style = getComputedStyle(sizeMatchBox, null); 424 | textarea.style.width = parseInt(style.width.substring(0, style.width.length - 2)) + 4 + "px"; 425 | textarea.style.height = parseInt(style.height.substring(0, style.height.length - 2)) + 4 + "px"; 426 | } 427 | 428 | function getText(element) { 429 | var result = ""; 430 | for (var i = 0; i < element.childNodes.length; i++) { 431 | var child = element.childNodes[i]; 432 | if (child.nodeType == 1) { 433 | var tagName = child.tagName.toLowerCase(); 434 | if (tagName == "br") { 435 | result += "\n"; 436 | continue; 437 | } 438 | if (child.tagName == "li") { 439 | result += "\n*\t"; 440 | } 441 | if (tagName == "p" 442 | || /^h[0-6]$/.test(tagName) 443 | || tagName == "header" 444 | || tagName == "aside" 445 | || tagName == "blockquote" 446 | || tagName == "footer" 447 | || tagName == "div" 448 | || tagName == "table" 449 | || tagName == "hr") { 450 | if (result != "") { 451 | result += "\n"; 452 | } 453 | } 454 | if (tagName == "td" || tagName == "th") { 455 | result += "\t"; 456 | } 457 | 458 | result += getText(child); 459 | 460 | if (tagName == "tr") { 461 | result += "\n"; 462 | } 463 | } else if (child.nodeType == 3) { 464 | result += child.nodeValue; 465 | } 466 | } 467 | result = result.replace("\r\n", "\n"); 468 | return result; 469 | } 470 | 471 | // Edit string 472 | Jsonary.render.register({ 473 | renderHtml: function (data, context) { 474 | var maxLength = data.schemas().maxLength(); 475 | var inputName = context.inputNameForAction('new-value'); 476 | var valueHtml = escapeHtml(data.value()).replace('"', '"'); 477 | var style = ""; 478 | style += "width: 90%"; 479 | return ''; 482 | }, 483 | action: function (context, actionName, arg1) { 484 | if (actionName == 'new-value') { 485 | context.data.setValue(arg1); 486 | } 487 | }, 488 | render: function (element, data, context) { 489 | //Use contentEditable 490 | if (element.contentEditable !== null) { 491 | element.innerHTML = '
' + escapeHtml(data.value()).replace(/\n/g, "
") + '
'; 492 | var valueSpan = element.childNodes[0]; 493 | valueSpan.contentEditable = "true"; 494 | valueSpan.onblur = function () { 495 | var newString = getText(valueSpan); 496 | data.setValue(newString); 497 | }; 498 | return; 499 | } 500 | 501 | if (typeof window.getComputedStyle != "function") { 502 | return; 503 | } 504 | // min/max length 505 | var minLength = data.schemas().minLength(); 506 | var maxLength = data.schemas().maxLength(); 507 | var noticeBox = document.createElement("span"); 508 | noticeBox.className="json-string-notice"; 509 | function updateNoticeBox(stringValue) { 510 | if (stringValue.length < minLength) { 511 | noticeBox.innerHTML = 'Too short (minimum ' + minLength + ' characters)'; 512 | } else if (maxLength != null && stringValue.length > maxLength) { 513 | noticeBox.innerHTML = 'Too long (+' + (stringValue.length - maxLength) + ' characters)'; 514 | } else if (maxLength != null) { 515 | noticeBox.innerHTML = (maxLength - stringValue.length) + ' characters left'; 516 | } else { 517 | noticeBox.innerHTML = ""; 518 | } 519 | } 520 | 521 | // size match 522 | var sizeMatchBox = document.createElement("div"); 523 | 524 | var textarea = null; 525 | for (var i = 0; i < element.childNodes.length; i++) { 526 | if (element.childNodes[i].nodeType == 1) { 527 | textarea = element.childNodes[i]; 528 | break; 529 | } 530 | } 531 | element.insertBefore(sizeMatchBox, textarea); 532 | copyTextStyle(textarea, sizeMatchBox); 533 | sizeMatchBox.style.display = "inline"; 534 | sizeMatchBox.style.position = "absolute"; 535 | sizeMatchBox.style.width = "auto"; 536 | sizeMatchBox.style.height = "auto"; 537 | sizeMatchBox.style.left = "-100000px"; 538 | sizeMatchBox.style.top = "0px"; 539 | sizeMatchBox.style.whiteSpace = "pre"; 540 | sizeMatchBox.style.zIndex = -10000; 541 | var suffix = "MMMMM"; 542 | updateTextareaSize(textarea, sizeMatchBox, suffix); 543 | 544 | textarea.value = data.value(); 545 | textarea.onkeyup = function () { 546 | updateNoticeBox(this.value); 547 | updateTextareaSize(this, sizeMatchBox, suffix); 548 | }; 549 | textarea.onfocus = function () { 550 | updateNoticeBox(data.value()); 551 | suffix = "MMMMM\nMMM"; 552 | updateTextareaSize(this, sizeMatchBox, suffix); 553 | }; 554 | textarea.onblur = function () { 555 | data.setValue(this.value); 556 | noticeBox.innerHTML = ""; 557 | suffix = "MMMMM"; 558 | updateTextareaSize(this, sizeMatchBox, suffix); 559 | }; 560 | element.appendChild(noticeBox); 561 | textarea = null; 562 | element = null; 563 | }, 564 | update: function (element, data, context, operation) { 565 | if (element.contentEditable !== null) { 566 | var valueSpan = element.childNodes[0]; 567 | valueSpan.innerHTML = escapeHtml(data.value()).replace(/\n/g, "
"); 568 | return false; 569 | }; 570 | if (operation.action() == "replace") { 571 | var textarea = null; 572 | for (var i = 0; i < element.childNodes.length; i++) { 573 | if (element.childNodes[i].tagName.toLowerCase() == "textarea") { 574 | textarea = element.childNodes[i]; 575 | break; 576 | } 577 | } 578 | textarea.value = data.value(); 579 | textarea.onkeyup(); 580 | return false; 581 | } else { 582 | return true; 583 | } 584 | }, 585 | filter: function (data) { 586 | return data.basicType() == "string" && !data.readOnly(); 587 | } 588 | }); 589 | 590 | // Display/edit boolean 591 | Jsonary.render.register({ 592 | render: function (element, data) { 593 | var valueSpan = document.createElement("a"); 594 | if (data.value()) { 595 | valueSpan.setAttribute("class", "json-boolean-true"); 596 | valueSpan.innerHTML = "true"; 597 | } else { 598 | valueSpan.setAttribute("class", "json-boolean-false"); 599 | valueSpan.innerHTML = "false"; 600 | } 601 | element.appendChild(valueSpan); 602 | if (!data.readOnly()) { 603 | valueSpan.setAttribute("href", "#"); 604 | valueSpan.onclick = function (event) { 605 | data.setValue(!data.value()); 606 | return false; 607 | }; 608 | } 609 | valueSpan = null; 610 | element = null; 611 | }, 612 | filter: function (data) { 613 | return data.basicType() == "boolean"; 614 | } 615 | }); 616 | 617 | // Edit number 618 | Jsonary.render.register({ 619 | renderHtml: function (data, context) { 620 | var result = context.actionHtml('' + data.value() + '', "input"); 621 | 622 | var interval = data.schemas().numberInterval(); 623 | if (interval != undefined) { 624 | var minimum = data.schemas().minimum(); 625 | if (minimum == null || data.value() > minimum + interval || data.value() == (minimum + interval) && !data.schemas().exclusiveMinimum()) { 626 | result = context.actionHtml('-', 'decrement') + result; 627 | } 628 | 629 | var maximum = data.schemas().maximum(); 630 | if (maximum == null || data.value() < maximum - interval || data.value() == (maximum - interval) && !data.schemas().exclusiveMaximum()) { 631 | result += context.actionHtml('+', 'increment'); 632 | } 633 | } 634 | return result; 635 | }, 636 | action: function (context, actionName) { 637 | var data = context.data; 638 | var interval = data.schemas().numberInterval(); 639 | if (actionName == "increment") { 640 | data.setValue(data.value() + interval); 641 | } else if (actionName == "decrement") { 642 | data.setValue(data.value() - interval); 643 | } else if (actionName == "input") { 644 | var newValueString = prompt("Enter number: ", data.value()); 645 | var value = parseFloat(newValueString); 646 | if (!isNaN(value)) { 647 | if (interval != undefined) { 648 | value = Math.round(value/interval)*interval; 649 | } 650 | var valid = true; 651 | var minimum = data.schemas().minimum(); 652 | if (minimum != undefined) { 653 | if (value < minimum || (value == minimum && data.schemas().exclusiveMinimum())) { 654 | valid = false; 655 | } 656 | } 657 | var maximum = data.schemas().maximum(); 658 | if (maximum != undefined) { 659 | if (value > maximum || (value == maximum && data.schemas().exclusiveMaximum())) { 660 | valid = false; 661 | } 662 | } 663 | if (!valid) { 664 | value = data.schemas().createValueNumber(); 665 | } 666 | data.setValue(value); 667 | } 668 | } 669 | }, 670 | filter: function (data) { 671 | return (data.basicType() == "number" || data.basicType() == "integer") && !data.readOnly(); 672 | } 673 | }); 674 | 675 | // Edit enums 676 | Jsonary.render.register({ 677 | render: function (element, data, context) { 678 | var enumValues = data.schemas().enumValues(); 679 | if (enumValues.length == 0) { 680 | element.innerHTML = 'invalid'; 681 | return; 682 | } else if (enumValues.length == 1) { 683 | if (typeof enumValues[0] == "string") { 684 | element.innerHTML = '' + escapeHtml(enumValues[0]) + ''; 685 | } else if (typeof enumValues[0] == "number") { 686 | element.innerHTML = '' + enumValues[0] + ''; 687 | } else if (typeof enumValues[0] == "boolean") { 688 | var text = (enumValues[0] ? "true" : "false"); 689 | element.innerHTML = '' + text + ''; 690 | } else { 691 | element.innerHTML = '' + escapeHtml(JSON.stringify(enumValues[0])) + ''; 692 | } 693 | return; 694 | } 695 | var select = document.createElement("select"); 696 | for (var i = 0; i < enumValues.length; i++) { 697 | var option = document.createElement("option"); 698 | option.setAttribute("value", i); 699 | if (data.equals(Jsonary.create(enumValues[i]))) { 700 | option.selected = true; 701 | } 702 | option.appendChild(document.createTextNode(enumValues[i])); 703 | select.appendChild(option); 704 | } 705 | select.onchange = function () { 706 | var index = this.value; 707 | data.setValue(enumValues[index]); 708 | } 709 | element.appendChild(select); 710 | element = select = option = null; 711 | }, 712 | filter: function (data) { 713 | return !data.readOnly() && data.schemas().enumValues() != null; 714 | } 715 | }); 716 | 717 | })(); 718 | --------------------------------------------------------------------------------