From 1e765d295585ff07ecfdee0d9bda1ef40b918dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Urba=C5=84czyk?= Date: Mon, 11 Oct 2021 21:28:24 +0200 Subject: [PATCH] feat: render missed parts of info and tag objects (#187) --- components/Info.js | 121 ++++++++++++++++++++---- components/Message.js | 3 +- components/TableOfContents.js | 85 +++++++++++++++++ components/Tags.js | 44 +++++++++ components/common.js | 7 ++ template/asyncapi.js | 35 +------ test/components/Info.test.js | 118 +++++++++++++++++++++++ test/components/TableOfContents.test.js | 34 +++++++ test/components/Tags.test.js | 63 ++++++++++++ 9 files changed, 460 insertions(+), 50 deletions(-) create mode 100644 components/TableOfContents.js create mode 100644 components/Tags.js create mode 100644 test/components/Info.test.js create mode 100644 test/components/TableOfContents.test.js create mode 100644 test/components/Tags.test.js diff --git a/components/Info.js b/components/Info.js index 22297ada3..e769dc8d2 100644 --- a/components/Info.js +++ b/components/Info.js @@ -1,38 +1,121 @@ import { Text } from "@asyncapi/generator-react-sdk"; -import { Header, Link, Image } from "./common"; +import { Tags } from "./Tags"; +import { Header, Link, Image, List } from "./common"; -export function Info({ asyncapi, params }) { +export function Info({ asyncapi, params = {} }) { const info = asyncapi.info(); + + const specId = asyncapi.id(); + const externalDocs = asyncapi.externalDocs(); + const license = info.license(); + const termsOfService = info.termsOfService(); + const defaultContentType = asyncapi.defaultContentType(); + const contact = info.contact(); + + const infoList = []; + if (specId) { + infoList.push(`Specification ID: \`${specId}\``); + } + if (license) { + infoList.push(license.url() ? ( + <> + License:{' '} + + {license.name()} + + + ) : `License: ${license.name()}`); + } + if (termsOfService) { + infoList.push( + <> + Terms of service:{' '} + + {termsOfService} + + + ); + } + if (defaultContentType) { + infoList.push( + <> + Default content type:{' '} + + {defaultContentType} + + + ); + } + if (contact) { + contact.url() && infoList.push( + <> + Support:{' '} + + {contact.name() || 'Link'} + + + ); + contact.email() && infoList.push( + <> + Email support:{' '} + + {contact.email()} + + + ); + } + return ( - <> +
{info.title()} {params.version || info.version()} documentation
- {info.description() && ( + {info.hasExt('x-logo') && ( + + + + )} + + {infoList.length && ( + <> + + + + )} + + {externalDocs && ( + + + {externalDocs.hasDescription() ? externalDocs.description() : 'Find more info here.'} + + + )} + + {info.hasDescription() && ( {info.description()} )} - {info.hasExt('x-logo') && ( + {asyncapi.hasTags() && ( - + )} - + ); } - -export function TermsOfService({ asyncapi }) { - const termsOfService = asyncapi.info().termsOfService(); - return termsOfService ? ( - -
- Terms of service -
- {termsOfService} -
- ) : null; -} \ No newline at end of file diff --git a/components/Message.js b/components/Message.js index 767a055a7..2d3478663 100644 --- a/components/Message.js +++ b/components/Message.js @@ -1,8 +1,9 @@ import { Text } from "@asyncapi/generator-react-sdk"; import { generateExample, getPayloadExamples, getHeadersExamples } from "@asyncapi/generator-filters"; -import { Header, CodeBlock, BlockQuote, Tags } from "./common"; import { Schema } from "./Schema"; +import { Tags } from "./Tags"; +import { Header, CodeBlock, BlockQuote } from "./common"; export function Message({ message, title = 'Message' }) { return ( diff --git a/components/TableOfContents.js b/components/TableOfContents.js new file mode 100644 index 000000000..e2a3b0ddd --- /dev/null +++ b/components/TableOfContents.js @@ -0,0 +1,85 @@ +import { Text, Indent, IndentationTypes } from "@asyncapi/generator-react-sdk"; + +import { Header, Link, ListItem } from "../components/common"; + +export function TableOfContents({ asyncapi }) { + const serversList = Object.keys(asyncapi.servers()).map(serverName => { + return ( + + + {serverName} + + + ); + }); + const channelsList = Object.keys(asyncapi.channels()).map(channelName => { + return ( + + + {channelName} + + + ); + }); + + return ( + <> +
Table of Contents
+ + {asyncapi.hasServers() && + + Servers + + } + {serversList.length > 0 && serversList} + {asyncapi.hasChannels() && + + Channels + + } + {channelsList.length > 0 && channelsList} + + + ); +} + +/** + * Slugify (change value to appropriate hash) the url part of a markdown link. + * + * @param {String} `str` The string to slugify + * @return {String} + */ +function slugify(str) { + str = getTitle(str); + str = str.toLowerCase(); + + // `split(...).join(...)` is faster than `replace(..., ...)` + // for spaces + str = str.split(' ').join('-'); + // for tabs + str = str.split(/\t/).join('--'); + // for html tags + str = str.split(/<\/?[^>]{1,100}>/).join(''); + // for special characters from ASCII (part 1) + str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>.,;:'"^]/).join(''); + // for special characters from ASCII (part 2) + str = str.split(/[。?!,、;:【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join(''); + + return str; +} + +/** + * Get the "title" from a markdown link + * + * @param {String} `str` The string to retrieve title + * @return {String} + */ +function getTitle(str) { + // check if in `str` is "title" from a markdown link (use `(` char at the end for easy markdown link checking) + if (/^\[[^\]]+\]\(/.test(str)) { + // retrieve "title" from a markdown link + var m = /^\[([^\]]+)\]/.exec(str); + if (m) return m[1]; + } + return str; +} diff --git a/components/Tags.js b/components/Tags.js new file mode 100644 index 000000000..6ab8b4144 --- /dev/null +++ b/components/Tags.js @@ -0,0 +1,44 @@ +import { IndentationTypes, Text } from "@asyncapi/generator-react-sdk"; + +import { ListItem, Link } from "./common"; + +export function Tags({ tags = [] }) { + if (tags.length === 0) { + return null + } + + return ( + <> + {tags.map(tag => ( + + ))} + + ); +} + +function Tag({ tag }) { + const description = tag.description(); + const externalDocs = tag.externalDocs(); + + return ( + <> + + {tag.name()} + + {description && ( + + {description} + + )} + {externalDocs && ( + + + {externalDocs.hasDescription() ? externalDocs.description() : 'Find more info here.'} + + + )} + + ); +} diff --git a/components/common.js b/components/common.js index d5942380a..153412fdd 100644 --- a/components/common.js +++ b/components/common.js @@ -13,6 +13,13 @@ export function Image({ src = "", desc = "", childrenContent = "" }) { return `![${desc || childrenContent}](${src})`; } +export function List({ list = [] }) { + if (list.length === 0) return null; + return list.map(item => ( + {item} + )); +} + export function ListItem({ type = "*", childrenContent = "" }) { return {`${type} ${childrenContent}`}; } diff --git a/template/asyncapi.js b/template/asyncapi.js index 229700ef8..70a15034b 100644 --- a/template/asyncapi.js +++ b/template/asyncapi.js @@ -1,45 +1,20 @@ -import { File, Text } from "@asyncapi/generator-react-sdk"; +import { File } from "@asyncapi/generator-react-sdk"; -import { Header, Link, ListItem } from "../components/common"; -import { Info, TermsOfService } from "../components/Info"; +import { Info } from "../components/Info"; import { Servers } from "../components/Servers"; import { Channels } from "../components/Channels"; import { FrontMatter } from "../components/FrontMatter"; +import { TableOfContents } from "../components/TableOfContents"; -export default function({ asyncapi, params }) { +export default function({ asyncapi, params = {} }) { return ( {params.frontMatter && } {params.toc !== 'false' && } - + ); } - -function TableOfContents({ asyncapi }) { - return ( - <> -
Table of Contents
- - {asyncapi.info().termsOfService() && - - Terms of Service - - } - {asyncapi.hasServers() && - - Servers - - } - {asyncapi.hasChannels() && - - Channels - - } - - - ); -} \ No newline at end of file diff --git a/test/components/Info.test.js b/test/components/Info.test.js new file mode 100644 index 000000000..45d8991d1 --- /dev/null +++ b/test/components/Info.test.js @@ -0,0 +1,118 @@ +import { render } from '@asyncapi/generator-react-sdk'; + +import { Info } from "../../components/Info"; +import AsyncAPIDocument from '@asyncapi/parser/lib/models/asyncapi'; + +describe('Info component', () => { + it('should render all fields', () => { + const asyncapi = new AsyncAPIDocument({ + "asyncapi": "2.2.0", + "id": "urn:com:smartylighting:streetlights:server", + "info": { + "title": "Streetlights API", + "version": "1.0.0", + "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n### Check out its awesome features:\n* Turn a specific streetlight on/off 🌃\n* Dim a specific streetlight 😎\n* Receive real-time information about environmental lighting conditions 📈\n", + "termsOfService": "https://asyncapi.org/terms/", + "contact": { + "name": "API Support", + "url": "https://www.asyncapi.org/support", + "email": "support@asyncapi.org" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "tags": [ + { + "name": "root-tag1", + "externalDocs": { + "description": "External docs description 1", + "url": "https://www.asyncapi.com/" + } + }, + { + "name": "root-tag2", + "description": "Description 2", + "externalDocs": { + "url": "https://www.asyncapi.com/" + } + }, + { + "name": "root-tag3" + }, + { + "name": "root-tag4", + "description": "Description 4" + }, + { + "name": "root-tag5", + "externalDocs": { + "url": "https://www.asyncapi.com/" + } + } + ], + "externalDocs": { + "description": "More info here", + "url": "https://example.com" + }, + "defaultContentType": "application/json" + }); + const expected = ` +# Streetlights API 1.0.0 documentation + +* Specification ID: \`urn:com:smartylighting:streetlights:server\` +* License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) +* Terms of service: [https://asyncapi.org/terms/](https://asyncapi.org/terms/) +* Default content type: [application/json](https://www.iana.org/assignments/media-types/application/json) +* Support: [API Support](https://www.asyncapi.org/support) +* Email support: [support@asyncapi.org](mailto:support@asyncapi.org) + +[More info here](https://example.com) + +The Smartylighting Streetlights API allows you to remotely manage the city lights. +### Check out its awesome features: +* Turn a specific streetlight on/off 🌃 +* Dim a specific streetlight 😎 +* Receive real-time information about environmental lighting conditions 📈 + +* root-tag1 + + [External docs description 1](https://www.asyncapi.com/) +* root-tag2 + + Description 2 + [Find more info here.](https://www.asyncapi.com/) +* root-tag3 + +* root-tag4 + + Description 4 +* root-tag5 + + [Find more info here.](https://www.asyncapi.com/) +`; + + const result = render(); + expect(result.trim()).toEqual(expected.trim()); + }); + + it('should render logo from an extension', () => { + const asyncapi = new AsyncAPIDocument({ + "asyncapi": "2.2.0", + "info": { + "title": "Streetlights API", + "version": "1.0.0", + "x-logo": "example.com/image" + }, + }); + const expected = ` +# Streetlights API 1.0.0 documentation + +![Streetlights API logo](example.com/image) +`; + + const result = render(); + expect(result.trim()).toEqual(expected.trim()); + }); +}); diff --git a/test/components/TableOfContents.test.js b/test/components/TableOfContents.test.js new file mode 100644 index 000000000..6f20ce83a --- /dev/null +++ b/test/components/TableOfContents.test.js @@ -0,0 +1,34 @@ +import { render } from '@asyncapi/generator-react-sdk'; + +import { TableOfContents } from "../../components/TableOfContents"; +import AsyncAPIDocument from '@asyncapi/parser/lib/models/asyncapi'; + +describe('TableOfContents component', () => { + it('should render toc', () => { + const asyncapi = new AsyncAPIDocument({ + servers: { + production: {}, + testing: {}, + canary: {}, + }, + channels: { + testChannel: {}, + 'smartylighting/streetlights/1/0': {}, + }, + }); + const expected = ` +## Table of Contents + +* [Servers](#servers) + * [production](#production-server) + * [testing](#testing-server) + * [canary](#canary-server) +* [Channels](#channels) + * [testChannel](#testchannel-channel) + * [smartylighting/streetlights/1/0](#smartylightingstreetlights10-channel) +`; + + const result = render(); + expect(result.trim()).toEqual(expected.trim()); + }); +}); diff --git a/test/components/Tags.test.js b/test/components/Tags.test.js new file mode 100644 index 000000000..36fcd9975 --- /dev/null +++ b/test/components/Tags.test.js @@ -0,0 +1,63 @@ +import { render } from '@asyncapi/generator-react-sdk'; + +import { Tags } from "../../components/Tags"; +import TagModel from '@asyncapi/parser/lib/models/tag'; + +describe('Tags component', () => { + it('should render logo from an extension', () => { + const tags = [ + { + "name": "root-tag1", + "externalDocs": { + "description": "External docs description 1", + "url": "https://www.asyncapi.com/" + } + }, + { + "name": "root-tag2", + "description": "Description 2", + "externalDocs": { + "url": "https://www.asyncapi.com/" + } + }, + { + "name": "root-tag3" + }, + { + "name": "root-tag4", + "description": "Description 4" + }, + { + "name": "root-tag5", + "externalDocs": { + "url": "https://www.asyncapi.com/" + } + } + ].map(t => new TagModel(t)); + const expected = ` +* root-tag1 + + [External docs description 1](https://www.asyncapi.com/) +* root-tag2 + + Description 2 + [Find more info here.](https://www.asyncapi.com/) +* root-tag3 + +* root-tag4 + + Description 4 +* root-tag5 + + [Find more info here.](https://www.asyncapi.com/) +`; + + const result = render(); + expect(result.trim()).toEqual(expected.trim()); + }); + + it('should render nothing if tags prop is undefined', () => { + const result = render(); + expect(result).toEqual(''); + }); +});