From 19f4699ac49070958551348742308b0716090975 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 24 May 2017 15:14:58 -0700 Subject: [PATCH] feat(load): Allow caller to specify data format for an asset fetch When calling `load`, you may now explicitly state a data format / file extension. If you don't specify one, the default for that asset type will be used. Resolves #7 --- README.md | 4 ++-- src/Asset.js | 2 +- src/AssetType.js | 4 ++-- src/DataFormat.js | 3 ++- src/Helper.js | 5 ++-- src/LocalHelper.js | 7 +++--- src/ScratchStorage.js | 28 ++++++++++++++++------- src/WebHelper.js | 7 +++--- test/integration/download-known-assets.js | 26 +++++++++++++++++---- 9 files changed, 60 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e4b063d3..cb5c8cf4 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ var getAssetUrl = function (asset) { 'https://assets.example.com/path/to/assets/', asset.assetId, '.', - asset.assetType.runtimeFormat, + asset.dataFormat, '/get/' ]; return assetUrlParts.join(''); @@ -71,7 +71,7 @@ If you're using ES6 you may be able to simplify all of the above quite a bit: ```js storage.addWebSource( [AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], - asset => `https://assets.example.com/path/to/assets/${asset.assetId}.${asset.assetType.runtimeFormat}/get/`); + asset => `https://assets.example.com/path/to/assets/${asset.assetId}.${asset.dataFormat}/get/`); ``` Once the storage module is aware of the sources you need, you can start loading assets: diff --git a/src/Asset.js b/src/Asset.js index 900866d9..15fcfa66 100644 --- a/src/Asset.js +++ b/src/Asset.js @@ -25,7 +25,7 @@ class Asset { /** @type {string} */ this.assetId = assetId; - this.setData(data, dataFormat); + this.setData(data, dataFormat || assetType.runtimeFormat); /** @type {Asset[]} */ this.dependencies = []; diff --git a/src/AssetType.js b/src/AssetType.js index b6a8bb59..85165d0c 100644 --- a/src/AssetType.js +++ b/src/AssetType.js @@ -6,8 +6,8 @@ const DataFormat = require('./DataFormat'); * @typedef {Object} AssetType - Information about a supported asset type. * @property {string} contentType - the MIME type associated with this kind of data. Useful for data URIs, etc. * @property {string} name - The human-readable name of this asset type. - * @property {DataFormat} runtimeFormat - The format used for runtime, in-memory storage of this asset. For example, a - * project stored in SB2 format on disk will be returned as JSON when loaded into memory. + * @property {DataFormat} runtimeFormat - The default format used for runtime, in-memory storage of this asset. For + * example, a project stored in SB2 format on disk will be returned as JSON when loaded into memory. * @property {boolean} immutable - Indicates if the asset id is determined by the asset content. */ const AssetType = { diff --git a/src/DataFormat.js b/src/DataFormat.js index 3d2da56b..24066504 100644 --- a/src/DataFormat.js +++ b/src/DataFormat.js @@ -1,8 +1,9 @@ /** * Enumeration of the supported data formats. - * @type {Object.} + * @enum {string} */ const DataFormat = { + JPG: 'jpg', JSON: 'json', PNG: 'png', SB2: 'sb2', diff --git a/src/Helper.js b/src/Helper.js index 1b241d38..0813e3bc 100644 --- a/src/Helper.js +++ b/src/Helper.js @@ -11,10 +11,11 @@ class Helper { * Fetch an asset but don't process dependencies. * @param {AssetType} assetType - The type of asset to fetch. * @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc. + * @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc. * @return {Promise.} A promise for the contents of the asset. */ - load (assetType, assetId) { - return Promise.reject(new Error(`No asset of type ${assetType} for ID ${assetId}`)); + load (assetType, assetId, dataFormat) { + return Promise.reject(new Error(`No asset of type ${assetType} for ID ${assetId} with format ${dataFormat}`)); } } diff --git a/src/LocalHelper.js b/src/LocalHelper.js index 8d3cc938..32f95bbd 100644 --- a/src/LocalHelper.js +++ b/src/LocalHelper.js @@ -21,17 +21,18 @@ class LocalHelper extends Helper { * Fetch an asset but don't process dependencies. * @param {AssetType} assetType - The type of asset to fetch. * @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc. + * @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc. * @return {Promise.} A promise for the contents of the asset. */ - load (assetType, assetId) { + load (assetType, assetId, dataFormat) { return new Promise((fulfill, reject) => { - const fileName = [assetId, assetType.runtimeFormat].join('.'); + const fileName = [assetId, dataFormat].join('.'); localforage.getItem(fileName).then( data => { if (data === null) { fulfill(null); } else { - fulfill(new Asset(assetType, assetId, assetType.runtimeFormat, data)); + fulfill(new Asset(assetType, assetId, dataFormat, data)); } }, error => { diff --git a/src/ScratchStorage.js b/src/ScratchStorage.js index cf2e5d4f..11167806 100644 --- a/src/ScratchStorage.js +++ b/src/ScratchStorage.js @@ -1,9 +1,11 @@ -const Asset = require('./Asset'); -const AssetType = require('./AssetType'); const BuiltinHelper = require('./BuiltinHelper'); const LocalHelper = require('./LocalHelper'); const WebHelper = require('./WebHelper'); +const _Asset = require('./Asset'); +const _AssetType = require('./AssetType'); +const _DataFormat = require('./DataFormat'); + class ScratchStorage { constructor () { this.defaultAssetId = {}; @@ -20,7 +22,7 @@ class ScratchStorage { * @constructor */ get Asset () { - return Asset; + return _Asset; } /** @@ -28,7 +30,15 @@ class ScratchStorage { * @constructor */ get AssetType () { - return AssetType; + return _AssetType; + } + + /** + * @return {DataFormat} - the list of supported data formats. + * @constructor + */ + get DataFormat () { + return _DataFormat; } /** @@ -37,7 +47,7 @@ class ScratchStorage { * @constructor */ static get Asset () { - return Asset; + return _Asset; } /** @@ -46,7 +56,7 @@ class ScratchStorage { * @constructor */ static get AssetType () { - return AssetType; + return _AssetType; } /** @@ -94,23 +104,25 @@ class ScratchStorage { * Fetch an asset by type & ID. * @param {AssetType} assetType - The type of asset to fetch. This also determines which asset store to use. * @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc. + * @param {DataFormat} [dataFormat] - Optional: load this format instead of the AssetType's default. * @return {Promise.} A promise for the requested Asset. * If the promise is fulfilled with non-null, the value is the requested asset or a fallback. * If the promise is fulfilled with null, the desired asset could not be found with the current asset sources. * If the promise is rejected, there was an error on at least one asset source. HTTP 404 does not count as an * error here, but (for example) HTTP 403 does. */ - load (assetType, assetId) { + load (assetType, assetId, dataFormat) { /** @type {Helper[]} */ const helpers = [this.builtinHelper, this.localHelper, this.webHelper]; const errors = []; let helperIndex = 0; + dataFormat = dataFormat || assetType.runtimeFormat; return new Promise((fulfill, reject) => { const tryNextHelper = () => { if (helperIndex < helpers.length) { const helper = helpers[helperIndex++]; - helper.load(assetType, assetId) + helper.load(assetType, assetId, dataFormat) .then( asset => { if (asset === null) { diff --git a/src/WebHelper.js b/src/WebHelper.js index 5b4cce42..d7b13cee 100644 --- a/src/WebHelper.js +++ b/src/WebHelper.js @@ -38,14 +38,15 @@ class WebHelper extends Helper { * Fetch an asset but don't process dependencies. * @param {AssetType} assetType - The type of asset to fetch. * @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc. + * @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc. * @return {Promise.} A promise for the contents of the asset. */ - load (assetType, assetId) { + load (assetType, assetId, dataFormat) { /** @type {Array.<{url:string, result:*}>} List of URLs attempted & errors encountered. */ const errors = []; const sources = this.sources.slice(); - const asset = new Asset(assetType, assetId); + const asset = new Asset(assetType, assetId, dataFormat); let sourceIndex = 0; return new Promise((fulfill, reject) => { @@ -77,7 +78,7 @@ class WebHelper extends Helper { } tryNextSource(); } else { - asset.setData(response.body, assetType.runtimeFormat); + asset.setData(response.body, dataFormat); fulfill(asset); } }, diff --git a/test/integration/download-known-assets.js b/test/integration/download-known-assets.js index e1762386..5a0e2816 100644 --- a/test/integration/download-known-assets.js +++ b/test/integration/download-known-assets.js @@ -16,6 +16,7 @@ test('constructor', t => { * @typedef {object} AssetTestInfo * @property {AssetType} type - The type of the asset. * @property {string} id - The asset's unique ID. + * @property {DataFormat} [ext] - Optional: the asset's data format / file extension. */ const testAssets = [ { @@ -33,11 +34,29 @@ const testAssets = [ id: 'f88bf1935daea28f8ca098462a31dbb0', // cat1-a md5: 'f88bf1935daea28f8ca098462a31dbb0' }, + { + type: storage.AssetType.ImageVector, + id: '6e8bd9ae68fdb02b7e1e3df656a75635', // cat1-b + md5: '6e8bd9ae68fdb02b7e1e3df656a75635', + ext: storage.DataFormat.SVG + }, { type: storage.AssetType.ImageBitmap, id: '7e24c99c1b853e52f8e7f9004416fa34', // squirrel md5: '7e24c99c1b853e52f8e7f9004416fa34' }, + { + type: storage.AssetType.ImageBitmap, + id: '66895930177178ea01d9e610917f8acf', // bus + md5: '66895930177178ea01d9e610917f8acf', + ext: storage.DataFormat.PNG + }, + { + type: storage.AssetType.ImageBitmap, + id: 'fe5e3566965f9de793beeffce377d054', // building at MIT + md5: 'fe5e3566965f9de793beeffce377d054', + ext: storage.DataFormat.JPG + }, { type: storage.AssetType.Sound, id: '83c36d806dc92327b9e7049a565c6bff', // meow @@ -59,7 +78,7 @@ test('addWebSource', t => { t.doesNotThrow(() => { storage.addWebSource( [storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound], - asset => `https://cdn.assets.scratch.mit.edu/internalapi/asset/${asset.assetId}.${asset.assetType.runtimeFormat}/get/` + asset => `https://cdn.assets.scratch.mit.edu/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/` ); }); t.end(); @@ -82,12 +101,11 @@ test('load', t => { for (var i = 0; i < testAssets.length; ++i) { const assetInfo = testAssets[i]; - const promise = storage.load(assetInfo.type, assetInfo.id); + var promise = storage.load(assetInfo.type, assetInfo.id, assetInfo.ext); t.type(promise, 'Promise'); + promise = promise.then(asset => checkAsset(assetInfo, asset)); promises.push(promise); - - promise.then(asset => checkAsset(assetInfo, asset)); } return Promise.all(promises);