diff --git a/services/coincap/coincap-base.js b/services/coincap/coincap-base.js new file mode 100644 index 0000000000000000000000000000000000000000..7ad3dfb6c6f2ff36803eed37b3e7b559c7a29b38 --- /dev/null +++ b/services/coincap/coincap-base.js @@ -0,0 +1,22 @@ +import { BaseJsonService } from '../index.js' + +export default class BaseCoincapService extends BaseJsonService { + static category = 'other' + + static defaultBadgeData = { label: 'coincap' } + + // Doc this API. From https://docs.coincap.io/ + // example: https://api.coincap.io/v2/assets/bitcoin + + async fetch({ assetId, schema }) { + return this._requestJson({ + schema, + url: `https://api.coincap.io/v2/assets/${assetId}`, + errorMessages: { + 404: 'asset not found', + }, + }) + } +} + +export { BaseCoincapService } diff --git a/services/coincap/coincap-changepercent24hr.service.js b/services/coincap/coincap-changepercent24hr.service.js new file mode 100644 index 0000000000000000000000000000000000000000..29b853b8d499e5acbab297fb124e0916fb6b5b2a --- /dev/null +++ b/services/coincap/coincap-changepercent24hr.service.js @@ -0,0 +1,44 @@ +import Joi from 'joi' +import { floorCount } from '../color-formatters.js' +import BaseCoincapService from './coincap-base.js' + +const schema = Joi.object({ + data: Joi.object({ + changePercent24Hr: Joi.string() + .pattern(/[0-9]*\.[0-9]+/i) + .required(), + name: Joi.string().required(), + }).required(), +}).required() + +export default class CoincapChangePercent24HrUsd extends BaseCoincapService { + static route = { base: 'coincap/change-percent-24hr', pattern: ':assetId' } + + static examples = [ + { + title: 'Coincap (Change Percent 24Hr)', + namedParams: { assetId: 'bitcoin' }, + staticPreview: this.render({ + asset: { name: 'bitcoin', changePercent24Hr: '2.0670573674501840"' }, + }), + keywords: ['bitcoin', 'crypto', 'cryptocurrency'], + }, + ] + + static percentFormat(changePercent24Hr) { + return `${parseInt(changePercent24Hr).toFixed(2)}%` + } + + static render({ asset }) { + return { + label: `${asset.name}`.toLowerCase(), + message: this.percentFormat(asset.changePercent24Hr), + color: floorCount(asset.changePercent24Hr), + } + } + + async handle({ assetId }) { + const { data: asset } = await this.fetch({ assetId, schema }) + return this.constructor.render({ asset }) + } +} diff --git a/services/coincap/coincap-changepercent24hr.tester.js b/services/coincap/coincap-changepercent24hr.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..564553d289e0d04a6572126edee867ab0ef1884e --- /dev/null +++ b/services/coincap/coincap-changepercent24hr.tester.js @@ -0,0 +1,43 @@ +import { isPercentage } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('request for existing asset with positive') + .get('/bitcoin.json') + .intercept(nock => + nock('https://api.coincap.io') + .get('/v2/assets/bitcoin') + .reply(200, { + data: { changePercent24Hr: '1.4767080598737783', name: 'Bitcoin' }, + }) + ) + .expectBadge({ + label: 'bitcoin', + message: '1.00%', + color: 'brightgreen', + }) + +t.create('request for existing asset with negative') + .get('/bitcoin.json') + .intercept(nock => + nock('https://api.coincap.io') + .get('/v2/assets/bitcoin') + .reply(200, { + data: { changePercent24Hr: '-1.4767080598737783', name: 'Bitcoin' }, + }) + ) + .expectBadge({ + label: 'bitcoin', + message: '-1.00%', + color: 'red', + }) + +t.create('change percent 24hr').get('/bitcoin.json').expectBadge({ + label: 'bitcoin', + message: isPercentage, +}) + +t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({ + label: 'coincap', + message: 'asset not found', +}) diff --git a/services/coincap/coincap-priceusd.service.js b/services/coincap/coincap-priceusd.service.js new file mode 100644 index 0000000000000000000000000000000000000000..94c1e477c435f29367d9218e73a3ff7f4a2a547c --- /dev/null +++ b/services/coincap/coincap-priceusd.service.js @@ -0,0 +1,45 @@ +import Joi from 'joi' +import BaseCoincapService from './coincap-base.js' + +const schema = Joi.object({ + data: Joi.object({ + priceUsd: Joi.string() + .pattern(/[0-9]*\.[0-9]+/i) + .required(), + name: Joi.string().required(), + }).required(), +}).required() + +export default class CoincapPriceUsd extends BaseCoincapService { + static route = { base: 'coincap/price-usd', pattern: ':assetId' } + + static examples = [ + { + title: 'Coincap (Price USD)', + namedParams: { assetId: 'bitcoin' }, + staticPreview: this.render({ + asset: { name: 'bitcoin', priceUsd: '19116.0479117336250772' }, + }), + keywords: ['bitcoin', 'crypto', 'cryptocurrency'], + }, + ] + + static priceFormat(price) { + return `$${parseFloat(price) + .toFixed(2) + .replace(/\d(?=(\d{3})+\.)/g, '$&,')}` + } + + static render({ asset }) { + return { + label: `${asset.name}`.toLowerCase(), + message: this.priceFormat(asset.priceUsd), + color: 'blue', + } + } + + async handle({ assetId }) { + const { data: asset } = await this.fetch({ assetId, schema }) + return this.constructor.render({ asset }) + } +} diff --git a/services/coincap/coincap-priceusd.spec.js b/services/coincap/coincap-priceusd.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c099a06067aa7d2503d8833ec302326df6c04919 --- /dev/null +++ b/services/coincap/coincap-priceusd.spec.js @@ -0,0 +1,16 @@ +import { test, given } from 'sazerac' +import CoincapPriceUsd from './coincap-priceusd.service.js' + +describe('PriceUsd Format', function () { + test(CoincapPriceUsd.priceFormat, () => { + given('3').expect('$3.00') + given('33').expect('$33.00') + given('332').expect('$332.00') + given('3324').expect('$3,324.00') + given('332432').expect('$332,432.00') + given('332432.2').expect('$332,432.20') + given('332432.25').expect('$332,432.25') + given('332432432').expect('$332,432,432.00') + given('332432432.3432432').expect('$332,432,432.34') + }) +}) diff --git a/services/coincap/coincap-priceusd.tester.js b/services/coincap/coincap-priceusd.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..bb7acec283b4149077120796f803f3c8989c81cf --- /dev/null +++ b/services/coincap/coincap-priceusd.tester.js @@ -0,0 +1,29 @@ +import { isCurrency } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('request for existing asset') + .get('/bitcoin.json') + .intercept(nock => + nock('https://api.coincap.io') + .get('/v2/assets/bitcoin') + .reply(200, { + data: { priceUsd: '16417.7176754790740415', name: 'Bitcoin' }, + }) + ) + .expectBadge({ + label: 'bitcoin', + message: '$16,417.72', + color: 'blue', + }) + +t.create('price usd').get('/bitcoin.json').expectBadge({ + label: 'bitcoin', + message: isCurrency, + color: 'blue', +}) + +t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({ + label: 'coincap', + message: 'asset not found', +}) diff --git a/services/coincap/coincap-rank.service.js b/services/coincap/coincap-rank.service.js new file mode 100644 index 0000000000000000000000000000000000000000..7240b4ee9286a727dc0fa2fa3bababad3ccf6ab4 --- /dev/null +++ b/services/coincap/coincap-rank.service.js @@ -0,0 +1,37 @@ +import Joi from 'joi' +import BaseCoincapService from './coincap-base.js' + +const schema = Joi.object({ + data: Joi.object({ + rank: Joi.string() + .pattern(/^[0-9]+$/) + .required(), + name: Joi.string().required(), + }).required(), +}).required() + +export default class CoincapRank extends BaseCoincapService { + static route = { base: 'coincap/rank', pattern: ':assetId' } + + static examples = [ + { + title: 'Coincap (Rank)', + namedParams: { assetId: 'bitcoin' }, + staticPreview: this.render({ asset: { name: 'bitcoin', rank: '1' } }), + keywords: ['bitcoin', 'crypto', 'cryptocurrency'], + }, + ] + + static render({ asset }) { + return { + label: `${asset.name}`.toLowerCase(), + message: asset.rank, + color: 'blue', + } + } + + async handle({ assetId }) { + const { data: asset } = await this.fetch({ assetId, schema }) + return this.constructor.render({ asset }) + } +} diff --git a/services/coincap/coincap-rank.tester.js b/services/coincap/coincap-rank.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..3ee469a19c686c3862a2716ad7366d530bdfafcf --- /dev/null +++ b/services/coincap/coincap-rank.tester.js @@ -0,0 +1,29 @@ +import Joi from 'joi' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('request for existing asset') + .get('/bitcoin.json') + .intercept(nock => + nock('https://api.coincap.io') + .get('/v2/assets/bitcoin') + .reply(200, { data: { rank: '1', name: 'Bitcoin' } }) + ) + .expectBadge({ + label: 'bitcoin', + message: '1', + color: 'blue', + }) + +t.create('rank') + .get('/bitcoin.json') + .expectBadge({ + label: 'bitcoin', + message: Joi.number().integer().min(1).required(), + color: 'blue', + }) + +t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({ + label: 'coincap', + message: 'asset not found', +}) diff --git a/services/test-validators.js b/services/test-validators.js index 3983b46ac28655392e2f10d13c5c99689e1013f3..6c52ce8603b77eab04c6634e6d27ff62078b0806 100644 --- a/services/test-validators.js +++ b/services/test-validators.js @@ -94,10 +94,14 @@ const isZeroOverTimePeriod = withRegex( ) const isIntegerPercentage = withRegex(/^[1-9][0-9]?%|^100%|^0%$/) +const isIntegerPercentageNegative = withRegex(/^-?[1-9][0-9]?%|^100%|^0%$/) const isDecimalPercentage = withRegex(/^[0-9]+\.[0-9]*%$/) +const isDecimalPercentageNegative = withRegex(/^-?[0-9]+\.[0-9]*%$/) const isPercentage = Joi.alternatives().try( isIntegerPercentage, - isDecimalPercentage + isDecimalPercentage, + isIntegerPercentageNegative, + isDecimalPercentageNegative ) const isFileSize = withRegex( @@ -164,6 +168,16 @@ const isHumanized = Joi.string().regex( /[0-9a-z]+ (second|seconds|minute|minutes|hour|hours|day|days|month|months|year|years)/ ) +// $1,530,602.24 // true +// 1,530,602.24 // true +// $1,666.24$ // false +// ,1,666,88, // false +// 1.6.66,6 // false +// .1555. // false +const isCurrency = withRegex( + /(?=.*\d)^\$?(([1-9]\d{0,2}(,\d{3})*)|0)?(\.\d{1,2})?$/ +) + export { isSemver, isVPlusTripleDottedVersion, @@ -199,4 +213,5 @@ export { isOrdinalNumber, isOrdinalNumberDaily, isHumanized, + isCurrency, }