Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI specification #69

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions source/ui/MainView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import "./screens/UserSettings";
import "./screens/Home";
import "./screens/Tags";

import "./screens/Doc";

import Notification from "./composants/Notification";

Expand All @@ -44,6 +45,8 @@ export default class MainView extends router(i18n(withUser(LitElement))){
static "/ui/admin/.*" = ()=> html`<admin-panel></admin-panel>`;
@route()
static "/ui/scenes/:id/" = ({params}) => html`<scene-history name="${params.id}"></scene-history>`;
@route()
static "/ui/doc/.*" = () => html`<user-doc></user-doc>`

connectedCallback(): void {
super.connectedCallback();
Expand Down Expand Up @@ -72,6 +75,7 @@ export default class MainView extends router(i18n(withUser(LitElement))){

</form>
<nav-link .selected=${this.isActive("/ui/tags/")} href="/ui/tags/">Collections</nav-link>
<nav-link .selected=${this.isActive("/ui/doc/")} href="/ui/doc/">Documentation</nav-link>
${(this.user?.isAdministrator)?html`<nav-link .selected=${this.isActive("/ui/admin/")} href="/ui/admin/">${this.t("ui.administration")}</nav-link>`:""}
<div class="divider"></div>
<user-button .selected=${this.isActive("/ui/user/")} .user=${this.user}></user-button>
Expand Down
1 change: 1 addition & 0 deletions source/ui/composants/TagList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default class TagList extends LitElement{
.tags-list{
display: flex;
gap: 2px;
flex-wrap: wrap;
}

.tag, .add-tag{
Expand Down
6 changes: 6 additions & 0 deletions source/ui/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ declare module "*.webp" {
export default path;
}

declare module "*.yml"{
import type { Openapi2 } from 'screens/Doc/oas';
const definition :Openapi2;
export default definition;
}

// Webpack constant: build version
declare const ENV_VERSION: string;
// Webpack constant: true during development build
Expand Down
15 changes: 15 additions & 0 deletions source/ui/oas-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import showdown from "showdown";
import {parse} from 'yaml';

/**
* Simple one-shot yaml loader for webpack to use already-bundled yaml module
*/
export default function (source) {
const converter = new showdown.Converter();
// Apply some transformations to the source...
const obj = parse(source, (key, value)=>{
if(key === "description") return converter.makeHtml(value);
else return value;
});
return `export default ${JSON.stringify(obj)}`;
}
21 changes: 20 additions & 1 deletion source/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion source/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"devDependencies": {
"@types/showdown": "^2.0.0",
"lit-css-loader": "^2.0.0",
"showdown": "^2.1.0"
"showdown": "^2.1.0",
"yaml": "^2.4.5"
}
}
26 changes: 26 additions & 0 deletions source/ui/screens/Doc/DocHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LitElement, customElement, html } from "lit-element";
import i18n from "../../state/translate";


@customElement("doc-home")
export default class DocHome extends i18n(LitElement){

createRenderRoot() {
return this;
}

render(){
return html`<div>
<h4>Getting started</h4>
<p>
You can head over to the main <a target="_blank" href="https://ecorpus.eu">documentation reference</a>
to learn more about the eCorpus database or to the <a href="https://smithsonian.github.io/dpo-voyager/">DPO Voyager</a> website to learn more specifically about voyager's features.
</p>
<h4>Integrating eCorpus</h4>
<p>
Developers might want to check out the <a href="/ui/doc/api/">API doc</a> (work in progress) to start experimenting.
</p>
<p>
</div>`
}
}
230 changes: 230 additions & 0 deletions source/ui/screens/Doc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@

import { LitElement, css, customElement, html, property } from "lit-element";

import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../../styles/common.scss';
import apiStyles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../../styles/apidoc.scss';

import definitions from "./openapi.yml";
import "./DocHome";


import "../../composants/Icon";
import "../../composants/Button";
import "../../composants/TagList";

import { Method, Operation, Path, Parameters } from "./oas";
import { navigate, route, router } from "../../state/router";


function resolveRefs<T>(t :T):T{
if(typeof t !== "object") return t;
if(Array.isArray(t)) return t.map(i=>resolveRefs(i)) as T;
if(!("$ref" in t)) return t;
if(typeof t["$ref"] !== "string" || !t["$ref"].startsWith("#")){
console.warn("Bad ref :", t["$ref"]);
return t;
}
const refs = t["$ref"].slice(1).split("/");
let ptr = definitions;
for(let ref of refs){
if(!ref) continue;
if(typeof ptr[ref] === "undefined"){
console.warn("Bad ref : ", t["$ref"]);
return t;
}
ptr = ptr[ref];
}
return ptr as T;
}


const operations = resolveRefs(Object.entries(definitions.paths).map(([pathname, {parameters, summary, ...operations}])=>{
return Object.entries(operations).map((op)=>([pathname, op[0], parameters, op[1]]));
}).flat()) as Array<[string, Method, Parameters, Operation]>;




console.log("Definitions : ", typeof definitions, definitions);

@customElement("user-doc")
export default class UserDoc extends router(LitElement){
path = "/ui/doc";

@route()
static "/" = ()=> html`<doc-home></doc-home>`
@route()
static "/api/" = ({parent})=> UserDoc.renderTags(parent);
@route()
static "/api/:tag" = ({parent})=> UserDoc.renderTags(parent);

createRenderRoot() {
return this;
}


static renderTags(that :UserDoc){
return definitions.tags.map((t)=>{
const active = that.isActive(`/ui/doc/api/${t.name}`)
return html`<tag-block ?expanded=${active} @select=${that.onTagClick} name=${t.name} .description=${t.description}></tag-block>`
});
}

render(){
let selIndex = definitions.tags.findIndex(t=>this.isActive(t.name));
return html`
<h2>eCorpus Documentation</h2>
<div class="main-grid">
<div class="grid-header" style="display:flex">
<nav-link .selected=${this.isActive("/ui/doc/", true)} href="${this.path}/">Home</nav-link>
<nav-link .selected=${this.isActive("/ui/doc/api/")} href="${this.path}/api/">API Doc</nav-link>
</div>
<div class="grid-toolbar">
${this.isActive("/ui/doc/api")?html `
<div class="section">
<h4>Sections</h4>
<tag-list .selected=${selIndex} .tags=${definitions.tags.map(t=>t.name)} @click=${this.onTagClick}></tag-list>
</div>
<div class="section">
<ui-button @click=${this.download} class="btn-main" icon="save" text="download openAPI specification"></ui-button>
</div>
`:null}
</div>
<div class="grid-content section">
${this.renderContent()}
</div>

`;
}

onTagClick = (ev :CustomEvent<string>) =>{
navigate(this, this.isActive(`/ui/doc/api/${ev.detail}`)? `/ui/doc/api/`:`/ui/doc/api/${ev.detail}`);
}

download = ()=>{
const el = document.createElement("a");
el.setAttribute("href", `data:application/json;base64,`+btoa(JSON.stringify(definitions, null, 2)));
el.setAttribute("download", "openapi.json");
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}
}

@customElement("tag-block")
export class TagBlock extends LitElement{
@property({attribute: true, type: String})
name :string;
@property({attribute: false, type: String})
description :string;

@property({attribute: true, reflect: true, type: Boolean})
expanded: boolean;

@property({attribute:false, type: String})
selected ?:string;

handleClick = (e:MouseEvent)=>{
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent("select", {detail: this.name}));
}

handleSelect = (e:CustomEvent<string>)=>{

}

paths() :Array<[string, Path]> {
return Object.entries(definitions.paths);
}

operations() :Array<[string, Method, Parameters, Operation ]>{
return operations.filter(op=>op[3].tags?.indexOf(this.name)!= -1);
}

render(){
return html`<div class="tag-line">
<div class="tag-header" @click=${this.expanded?null:this.handleClick}>
<h4>${this.name}</h4>
<div class="tag-summary">
${unsafeHTML(this.description)}
</div>
<ui-icon @click=${this.handleClick} id="tag-caret" class="caret" name="caret-${this.expanded?"up":"down"}"></ui-icon>
</div>
<div class="tag-body">${this.expanded?this.operations().map(([pathname, method, parameters, operation])=>{
return html`<op-line ?expanded=${this.selected === operation.operationId} method=${method} pathname=${pathname} .parameters=${parameters} .operation=${operation}></op-line>`
}):null}</div>
</div>`
}
static styles = [
styles,
apiStyles,
];
}


@customElement("op-line")
export class OperationLine extends LitElement{
@property({attribute: true, reflect: true, type: String})
method :Method;
@property({attribute: true, reflect: true, type: String})
pathname :string;

@property({attribute: false, type: Object})
operation :Operation;

@property({attribute: false, type: Object})
parameters :Parameters;

@property({attribute: true, reflect: true, type: Boolean})
expanded :boolean = false;

createRenderRoot() {
return this;
}

connectedCallback(): void {
this.classList.add("path-line");
super.connectedCallback();
}

protected update(changedProperties: Map<string | number | symbol, unknown>): void {
if(changedProperties.has("pathname")){
this.id = this.pathname;
}
super.update(changedProperties);
}


onclick = (ev: MouseEvent) => {
ev.stopPropagation();
this.expanded = !this.expanded;
}

render(){
if(this.expanded) console.log("Operation :", this.parameters, this.operation);
const methodName = (this.method.startsWith("x-"))? this.method.slice(2) :this.method;
return html`
<span class="method ${methodName}">
${methodName}
</span>
<span class="pathname">
${this.pathname}
${(this.expanded && this.parameters?.length)? html`
<h4>Parameters</h4>
${this.parameters.map(p=>html`<div class="operation-parameters">
<h5>${p.name}</h5>
<div>
${unsafeHTML(p.description)}
</div>
</div>`)}

`:null}
</span>
<span class="op-summary">${unsafeHTML(this.operation.description)}</span>
<ui-icon id="tag-caret" class="caret" name="caret-${this.expanded?"up":"down"}"></ui-icon>
`;
}
}
Loading
Loading