1
0
mirror of https://github.com/oceanprotocol-archive/squid-js.git synced 2024-02-02 15:31:51 +01:00

update metadata to OEP-8 v0.4

This commit is contained in:
Matthias Kretschmann 2019-08-15 13:23:56 +02:00
parent ba542f299a
commit 07d17fe32e
Signed by: m
GPG Key ID: 606EEEF3C479A91F
15 changed files with 221 additions and 190 deletions

View File

@ -72,7 +72,7 @@ describe('Asset Owners', () => {
// Granting access // Granting access
try { try {
await account2.requestTokens( await account2.requestTokens(
+metadata.base.price * +metadata.main.price *
10 ** -(await ocean.keeper.token.decimals()) 10 ** -(await ocean.keeper.token.decimals())
) )
} catch {} } catch {}

View File

@ -31,7 +31,7 @@ describe('Consume Asset', () => {
} }
}) })
it('should regiester a asset', async () => { it('should register an asset', async () => {
ddo = await ocean.assets.create(metadata as any, publisher) ddo = await ocean.assets.create(metadata as any, publisher)
assert.isDefined(ddo, 'Register has not returned a DDO') assert.isDefined(ddo, 'Register has not returned a DDO')
@ -50,7 +50,7 @@ describe('Consume Asset', () => {
it('should be able to request tokens for consumer', async () => { it('should be able to request tokens for consumer', async () => {
const initialBalance = (await consumer.getBalance()).ocn const initialBalance = (await consumer.getBalance()).ocn
const claimedTokens = const claimedTokens =
+metadata.base.price * 10 ** -(await ocean.keeper.token.decimals()) +metadata.main.price * 10 ** -(await ocean.keeper.token.decimals())
try { try {
await consumer.requestTokens(claimedTokens) await consumer.requestTokens(claimedTokens)
@ -115,7 +115,7 @@ describe('Consume Asset', () => {
it('should lock the payment by the consumer', async () => { it('should lock the payment by the consumer', async () => {
const paid = await ocean.agreements.conditions.lockReward( const paid = await ocean.agreements.conditions.lockReward(
serviceAgreementSignatureResult.agreementId, serviceAgreementSignatureResult.agreementId,
ddo.findServiceByType('Metadata').metadata.base.price, ddo.findServiceByType('Metadata').metadata.main.price,
consumer consumer
) )
@ -182,7 +182,7 @@ describe('Consume Asset', () => {
) )
}) })
it('should consume and store one assets', async () => { it('should consume and store one asset', async () => {
const accessService = ddo.findServiceByType('Access') const accessService = ddo.findServiceByType('Access')
const folder = '/tmp/ocean/squid-js-2' const folder = '/tmp/ocean/squid-js-2'

View File

@ -54,7 +54,7 @@ describe('Consume Asset (Brizo)', () => {
try { try {
await consumer.requestTokens( await consumer.requestTokens(
+metadata.base.price * +metadata.main.price *
10 ** -(await ocean.keeper.token.decimals()) 10 ** -(await ocean.keeper.token.decimals())
) )
} catch {} } catch {}

View File

@ -30,8 +30,8 @@ xdescribe('Consume Asset (Large size)', () => {
} }
metadata = { metadata = {
...baseMetadata, ...baseMetadata,
base: { main: {
...baseMetadata.base, ...baseMetadata.main,
files: [ files: [
{ {
url: 'https://speed.hetzner.de/1GB.bin' url: 'https://speed.hetzner.de/1GB.bin'
@ -52,7 +52,7 @@ xdescribe('Consume Asset (Large size)', () => {
try { try {
await consumer.requestTokens( await consumer.requestTokens(
+metadata.base.price * +metadata.main.price *
10 ** -(await ocean.keeper.token.decimals()) 10 ** -(await ocean.keeper.token.decimals())
) )
} catch {} } catch {}

View File

@ -45,7 +45,7 @@ describe('Search Asset', () => {
} }
}) })
it('should regiester some a asset', async () => { it('should register an asset', async () => {
assert.instanceOf( assert.instanceOf(
await ocean.assets.create( await ocean.assets.create(
metadataGenerator('Test1') as any, metadataGenerator('Test1') as any,

View File

@ -64,7 +64,7 @@ describe('Signature', () => {
{ {
type: 'Metadata', type: 'Metadata',
metadata: { metadata: {
base: { main: {
price: 10 price: 10
} }
} }

View File

@ -11,7 +11,7 @@ describe('Versions', () => {
ocean = await Ocean.getInstance(config) ocean = await Ocean.getInstance(config)
}) })
it('should returns the versions', async () => { it('should return the versions', async () => {
const versions = await ocean.versions.get() const versions = await ocean.versions.get()
assert.equal(versions.aquarius.status, OceanPlatformTechStatus.Working) assert.equal(versions.aquarius.status, OceanPlatformTechStatus.Working)

View File

@ -1,15 +1,30 @@
import { MetaData } from '../../src' // @oceanprotocol/squid import { MetaData } from '../../src' // @oceanprotocol/squid
const metadata: Partial<MetaData> = { const metadata: Partial<MetaData> = {
base: { main: {
name: undefined, name: undefined,
type: 'dataset', type: 'dataset',
description:
'Weather information of UK including temperature and humidity',
dateCreated: '2012-10-10T17:00:00Z', dateCreated: '2012-10-10T17:00:00Z',
datePublished: '2012-10-10T17:00:00Z', datePublished: '2012-10-10T17:00:00Z',
author: 'Met Office', author: 'Met Office',
license: 'CC-BY', license: 'CC-BY',
price: '21' + '0'.repeat(18),
files: [
{
index: 0,
url:
'https://raw.githubusercontent.com/oceanprotocol/squid-js/master/package.json'
},
{
index: 1,
url:
'https://raw.githubusercontent.com/oceanprotocol/squid-js/master/README.md'
}
]
},
additionalInformation: {
description:
'Weather information of UK including temperature and humidity',
copyrightHolder: 'Met Office', copyrightHolder: 'Met Office',
workExample: workExample:
'423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68', '423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68',
@ -27,20 +42,7 @@ const metadata: Partial<MetaData> = {
], ],
inLanguage: 'en', inLanguage: 'en',
categories: ['Economy', 'Data Science'], categories: ['Economy', 'Data Science'],
tags: ['weather', 'uk', '2011', 'temperature', 'humidity'], tags: ['weather', 'uk', '2011', 'temperature', 'humidity']
price: '21' + '0'.repeat(18),
files: [
{
index: 0,
url:
'https://raw.githubusercontent.com/oceanprotocol/squid-js/master/package.json'
},
{
index: 1,
url:
'https://raw.githubusercontent.com/oceanprotocol/squid-js/master/README.md'
}
]
} }
} }
@ -49,10 +51,13 @@ export const generateMetadata = (
price?: number price?: number
): Partial<MetaData> => ({ ): Partial<MetaData> => ({
...metadata, ...metadata,
base: { main: {
...metadata.base, ...metadata.main,
name, name,
price: (price || 21) + '0'.repeat(18) price: (price || 21) + '0'.repeat(18)
},
additionalInformation: {
...metadata.additionalInformation
} }
}) })

View File

@ -102,7 +102,7 @@ export class DDO {
*/ */
public getChecksum(): string { public getChecksum(): string {
const { metadata } = this.findServiceByType('Metadata') const { metadata } = this.findServiceByType('Metadata')
const { files, name, author, license } = metadata.base const { files, name, author, license } = metadata.main
const values = [ const values = [
...(files || []).map(({ checksum }) => checksum).filter(_ => !!_), ...(files || []).map(({ checksum }) => checksum).filter(_ => !!_),
@ -121,7 +121,7 @@ export class DDO {
* Generates proof using personal sing. * Generates proof using personal sing.
* @param {Web3} web3 Web3 instance. * @param {Web3} web3 Web3 instance.
* @param {string} publicKey Public key to be used on personal sign. * @param {string} publicKey Public key to be used on personal sign.
* @param {string} password Password if it's requirted. * @param {string} password Password if it's required.
* @return {Promise<Proof>} Proof object. * @return {Promise<Proof>} Proof object.
*/ */
public async generateProof( public async generateProof(
@ -150,11 +150,11 @@ export class DDO {
*/ */
public addChecksum(): void { public addChecksum(): void {
const metadataService = this.findServiceByType('Metadata') const metadataService = this.findServiceByType('Metadata')
if (metadataService.metadata.base.checksum) { if (metadataService.metadata.main.checksum) {
LoggerInstance.log('Checksum already exists') LoggerInstance.log('Checksum already exists')
return return
} }
metadataService.metadata.base.checksum = this.getChecksum() metadataService.metadata.main.checksum = this.getChecksum()
} }
/** /**

View File

@ -1,8 +1,4 @@
export interface StageRequirements { export interface StageRequirements {
computeServiceId?: string
serviceDefinitionId?: string
serverId?: string
serverInstances?: string
container: { container: {
image: string image: string
tag: string tag: string
@ -23,7 +19,7 @@ export interface StageOutput {
metadataUrl: string metadataUrl: string
secretStoreUrl: string secretStoreUrl: string
accessProxyUrl: string accessProxyUrl: string
metadata: MetaDataBase metadata: MetaDataMain
} }
export interface Stage { export interface Stage {
@ -39,6 +35,38 @@ export interface Workflow {
stages: Stage[] stages: Stage[]
} }
export interface Algorithm {
language: string
format?: string
version?: string
entrypoint: string
requirements: {
requirement: string
version: string
}
}
export interface ServiceDefinition {
auth: {
type: string
user?: string
password?: string
token?: string
}
endpoints: {
index: number
url: string
method: string
contentTypes: string[]
}
}
export interface Service {
spec?: string
specChecksum?: string
definition: ServiceDefinition
}
export interface File { export interface File {
/** /**
* File name. * File name.
@ -105,10 +133,10 @@ export interface File {
} }
/** /**
* Base attributes of Assets Metadata. * Main attributes of assets metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8#base-attributes * @see https://github.com/oceanprotocol/OEPs/tree/master/8
*/ */
export interface MetaDataBase { export interface MetaDataMain {
/** /**
* Descriptive name of the Asset. * Descriptive name of the Asset.
* @type {string} * @type {string}
@ -124,14 +152,6 @@ export interface MetaDataBase {
*/ */
type: 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other' type: 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
/**
* Details of what the resource is. For a dataset, this attribute
* explains what the data represents and what it can be used for.
* @type {string}
* @example "Weather information of UK including temperature and humidity"
*/
description?: string
/** /**
* The date on which the asset was created by the originator in * The date on which the asset was created by the originator in
* ISO 8601 format, Coordinated Universal Time. * ISO 8601 format, Coordinated Universal Time.
@ -164,6 +184,83 @@ export interface MetaDataBase {
*/ */
license: string license: string
/**
* Price of the asset.
* @type {string}
* @example "1000000000000000000"
*/
price: string
/**
* Array of File objects including the encrypted file urls and some additional information.
* @type {File[]}
*/
files: File[]
/**
* SHA3 hash of concatenated values: [list of all file checksums] + name + author + license + did
* @type {string}
*/
checksum?: string
encryptedFiles?: any
encryptedService?: any
workflow?: Workflow
algorithm?: Algorithm
service?: Service
}
/**
* Curation attributes of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8
*/
export interface Curation {
/**
* Decimal value between 0 and 1. 0 is the default value.
* @type {number}
* @example 0.93
*/
rating: number
/**
* Number of votes. 0 is the default value.
* @type {number}
* @example 123
*/
numVotes: number
/**
* Schema applied to calculate the rating.
* @type {string}
* @example "Binary Voting"
*/
schema?: string
/**
* Flag unsuitable content.
* @type {boolean}
* @example true
*/
isListed?: boolean
}
/**
* Additional Information of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8#additional-information
*/
export interface AdditionalInformation {
/**
* Details of what the resource is. For a dataset, this attribute
* explains what the data represents and what it can be used for.
* @type {string}
* @example "Weather information of UK including temperature and humidity"
*/
description?: string
/** /**
* The party holding the legal copyright. Empty by default. * The party holding the legal copyright. Empty by default.
* @type {string} * @type {string}
@ -220,62 +317,6 @@ export interface MetaDataBase {
*/ */
tags?: string[] tags?: string[]
/**
* Price of the asset.
* @type {string}
* @example "1000000000000000000"
*/
price: string
/**
* Array of File objects including the encrypted file urls and some additional information.
* @type {File[]}
*/
files: File[]
/**
* SHA3 hash of concatenated values: [list of all file checksums] + name + author + license + did
* @type {string}
*/
checksum?: string
encryptedFiles?: any
workflow?: Workflow
}
/**
* Curation attributes of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8#curation-attributes
*/
export interface Curation {
/**
* Decimal value between 0 and 1. 0 is the default value.
* @type {number}
* @example 0.93
*/
rating: number
/**
* Number of votes. 0 is the default value.
* @type {number}
* @example 123
*/
numVotes: number
/**
* Schema applied to calculate the rating.
* @type {string}
* @example "Binary Voting"
*/
schema?: string
}
/**
* Additional Information of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8#additional-information
*/
export interface AdditionalInformation {
/** /**
* An indication of update latency - i.e. How often are updates expected (seldom, * An indication of update latency - i.e. How often are updates expected (seldom,
* annually, quarterly, etc.), or is the resource static that is never expected * annually, quarterly, etc.), or is the resource static that is never expected
@ -283,28 +324,21 @@ export interface AdditionalInformation {
* @type {string} * @type {string}
* @example "yearly" * @example "yearly"
*/ */
updateFrequency: string updateFrequency?: string
/** /**
* A link to machine-readable structured markup (such as ttl/json-ld/rdf) * A link to machine-readable structured markup (such as ttl/json-ld/rdf)
* describing the dataset. * describing the dataset.
* @type {StructuredMarkup[]} * @type {StructuredMarkup[]}
*/ */
structuredMarkup: { structuredMarkup?: {
uri: string uri: string
mediaType: string mediaType: string
}[] }[]
/**
* Checksum of attributes to be able to compare if there are changes in
* the asset that you are purchasing.
* @type {string}
*/
checksum: string
} }
export interface MetaData { export interface MetaData {
main: MetaDataMain
additionalInformation?: AdditionalInformation additionalInformation?: AdditionalInformation
base: MetaDataBase
curation?: Curation curation?: Curation
} }

View File

@ -59,7 +59,7 @@ export class EscrowAccessSecretStoreTemplate extends AgreementTemplate {
) { ) {
return !!(await this.createFullAgreement( return !!(await this.createFullAgreement(
ddo.shortId(), ddo.shortId(),
ddo.findServiceByType('Metadata').metadata.base.price, ddo.findServiceByType('Metadata').metadata.main.price,
consumer, consumer,
from, from,
agreementId agreementId
@ -79,7 +79,7 @@ export class EscrowAccessSecretStoreTemplate extends AgreementTemplate {
} = await this.createFullAgreementData( } = await this.createFullAgreementData(
agreementId, agreementId,
ddo.shortId(), ddo.shortId(),
ddo.findServiceByType('Metadata').metadata.base.price, ddo.findServiceByType('Metadata').metadata.main.price,
consumer consumer
) )
return [ return [

View File

@ -81,7 +81,7 @@ export class OceanAssets extends Instantiable {
observer.next(CreateProgressStep.EncryptingFiles) observer.next(CreateProgressStep.EncryptingFiles)
const encryptedFiles = await this.ocean.secretStore.encrypt( const encryptedFiles = await this.ocean.secretStore.encrypt(
did.getId(), did.getId(),
metadata.base.files, metadata.main.files,
publisher publisher
) )
this.logger.log('Files encrypted') this.logger.log('Files encrypted')
@ -135,11 +135,11 @@ export class OceanAssets extends Instantiable {
// Overwrites defaults // Overwrites defaults
...metadata, ...metadata,
// Cleaning not needed information // Cleaning not needed information
base: { main: {
...metadata.base, ...metadata.main,
contentUrls: undefined, contentUrls: undefined,
encryptedFiles, encryptedFiles,
files: metadata.base.files.map( files: metadata.main.files.map(
(file, index) => ({ (file, index) => ({
...file, ...file,
index, index,
@ -165,7 +165,7 @@ export class OceanAssets extends Instantiable {
})) as Service[] })) as Service[]
}) })
// Overwritte initial service agreement conditions // Overwrite initial service agreement conditions
const rawConditions = await templates.escrowAccessSecretStoreTemplate.getServiceAgreementTemplateConditions() const rawConditions = await templates.escrowAccessSecretStoreTemplate.getServiceAgreementTemplateConditions()
const conditions = fillConditionsWithDDO(rawConditions, ddo) const conditions = fillConditionsWithDDO(rawConditions, ddo)
serviceAgreementTemplate.conditions = conditions serviceAgreementTemplate.conditions = conditions
@ -237,7 +237,7 @@ export class OceanAssets extends Instantiable {
const accessService = ddo.findServiceById(serviceDefinitionId) const accessService = ddo.findServiceById(serviceDefinitionId)
const { files } = metadata.base const { files } = metadata.main
const { serviceEndpoint } = accessService const { serviceEndpoint } = accessService
@ -265,7 +265,7 @@ export class OceanAssets extends Instantiable {
} else { } else {
const files = await this.ocean.secretStore.decrypt( const files = await this.ocean.secretStore.decrypt(
did, did,
ddo.findServiceByType('Metadata').metadata.base.encryptedFiles, ddo.findServiceByType('Metadata').metadata.main.encryptedFiles,
consumerAccount, consumerAccount,
ddo.findServiceByType('Authorization').serviceEndpoint ddo.findServiceByType('Authorization').serviceEndpoint
) )
@ -327,7 +327,7 @@ export class OceanAssets extends Instantiable {
observer.next(OrderProgressStep.LockingPayment) observer.next(OrderProgressStep.LockingPayment)
const paid = await oceanAgreements.conditions.lockReward( const paid = await oceanAgreements.conditions.lockReward(
agreementId, agreementId,
metadata.base.price, metadata.main.price,
consumer consumer
) )
observer.next(OrderProgressStep.LockedPayment) observer.next(OrderProgressStep.LockedPayment)

View File

@ -13,7 +13,7 @@ function fillParameterWithDDO(
case 'amount': case 'amount':
case 'price': case 'price':
return String( return String(
ddo.findServiceByType('Metadata').metadata.base.price ddo.findServiceByType('Metadata').metadata.main.price
) )
case 'assetId': case 'assetId':
case 'documentId': case 'documentId':

View File

@ -91,41 +91,13 @@ describe('DDO', () => {
serviceEndpoint: serviceEndpoint:
'http://myaquarius.org/api/v1/provider/assets/metadata/{did}', 'http://myaquarius.org/api/v1/provider/assets/metadata/{did}',
metadata: { metadata: {
base: { main: {
name: 'UK Weather information 2011', name: 'UK Weather information 2011',
type: 'dataset', type: 'dataset',
description:
'Weather information of UK including temperature and humidity',
dateCreated: '2012-10-10T17:00:000Z', dateCreated: '2012-10-10T17:00:000Z',
datePublished: '2012-10-10T17:00:000Z', datePublished: '2012-10-10T17:00:000Z',
author: 'Met Office', author: 'Met Office',
license: 'CC-BY', license: 'CC-BY',
copyrightHolder: 'Met Office',
workExample:
'423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68',
links: [
{
sample1:
'http://data.ceda.ac.uk/badc/ukcp09/data/gridded-land-obs/gridded-land-obs-daily/'
},
{
sample2:
'http://data.ceda.ac.uk/badc/ukcp09/data/gridded-land-obs/gridded-land-obs-averages-25km/'
},
{
fieldsDescription:
'http://data.ceda.ac.uk/badc/ukcp09/'
}
],
inLanguage: 'en',
categories: ['Economy', 'Data Science'],
tags: [
'weather',
'uk',
'2011',
'temperature',
'humidity'
],
price: 10, price: 10,
files: [ files: [
{ {
@ -152,7 +124,35 @@ describe('DDO', () => {
schema: 'Binary Voting' schema: 'Binary Voting'
}, },
additionalInformation: { additionalInformation: {
updateFrecuency: 'yearly', description:
'Weather information of UK including temperature and humidity',
copyrightHolder: 'Met Office',
workExample:
'423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68',
links: [
{
sample1:
'http://data.ceda.ac.uk/badc/ukcp09/data/gridded-land-obs/gridded-land-obs-daily/'
},
{
sample2:
'http://data.ceda.ac.uk/badc/ukcp09/data/gridded-land-obs/gridded-land-obs-averages-25km/'
},
{
fieldsDescription:
'http://data.ceda.ac.uk/badc/ukcp09/'
}
],
inLanguage: 'en',
categories: ['Economy', 'Data Science'],
tags: [
'weather',
'uk',
'2011',
'temperature',
'humidity'
],
updateFrequency: 'yearly',
structuredMarkup: [ structuredMarkup: [
{ {
uri: uri:

View File

@ -58,9 +58,7 @@
}, },
"events": { "events": {
"PaymentLocked": { "PaymentLocked": {
"actorType": [ "actorType": ["publisher"],
"publisher"
],
"handlers": [ "handlers": [
{ {
"moduleName": "accessControl", "moduleName": "accessControl",
@ -84,9 +82,7 @@
}, },
"events": { "events": {
"PaymentReleased": { "PaymentReleased": {
"actorType": [ "actorType": ["publisher"],
"publisher"
],
"handlers": [ "handlers": [
{ {
"moduleName": "serviceAgreement", "moduleName": "serviceAgreement",
@ -110,9 +106,7 @@
}, },
"events": { "events": {
"AccessGranted": { "AccessGranted": {
"actorType": [ "actorType": ["consumer"],
"consumer"
],
"handlers": [ "handlers": [
{ {
"moduleName": "asset", "moduleName": "asset",
@ -136,9 +130,7 @@
}, },
"events": { "events": {
"PaymentRefund": { "PaymentRefund": {
"actorType": [ "actorType": ["consumer"],
"consumer"
],
"handlers": [ "handlers": [
{ {
"moduleName": "serviceAgreement", "moduleName": "serviceAgreement",
@ -165,15 +157,10 @@
"base": { "base": {
"name": "UK Weather information 2011", "name": "UK Weather information 2011",
"type": "dataset", "type": "dataset",
"description": "Weather information of UK including temperature and humidity",
"dateCreated": "2012-10-10T17:00:000Z", "dateCreated": "2012-10-10T17:00:000Z",
"author": "Met Office", "author": "Met Office",
"license": "CC-BY", "license": "CC-BY",
"copyrightHolder": "Met Office", "price": 10,
"workExample": "423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68",
"contentUrls": [
"https://testocnfiles.blob.core.windows.net/testfiles/testzkp.zip"
],
"files": [ "files": [
{ {
"index": 0, "index": 0,
@ -184,6 +171,19 @@
{ {
"url": "https://testocnfiles.blob.core.windows.net/testfiles/testzkp2.zip" "url": "https://testocnfiles.blob.core.windows.net/testfiles/testzkp2.zip"
} }
]
},
"curation": {
"rating": 0.93,
"numVotes": 123,
"schema": "Binary Voting"
},
"additionalInformation": {
"description": "Weather information of UK including temperature and humidity",
"copyrightHolder": "Met Office",
"workExample": "423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68",
"contentUrls": [
"https://testocnfiles.blob.core.windows.net/testfiles/testzkp.zip"
], ],
"links": [ "links": [
{ {
@ -200,14 +200,6 @@
"inLanguage": "en", "inLanguage": "en",
"categories": ["Economy", "Data Science"], "categories": ["Economy", "Data Science"],
"tags": ["weather", "uk", "2011", "temperature", "humidity"], "tags": ["weather", "uk", "2011", "temperature", "humidity"],
"price": 10
},
"curation": {
"rating": 0.93,
"numVotes": 123,
"schema": "Binary Voting"
},
"additionalInformation": {
"updateFrequency": "yearly", "updateFrequency": "yearly",
"structuredMarkup": [ "structuredMarkup": [
{ {