Skip to content

Commit

Permalink
OpenAPI Explorer displays a set of API definitions on the web. (#1)
Browse files Browse the repository at this point in the history
The API listing file (apis.json) can be generated using `tree` and `jq`.
Instructions are in the README.
  • Loading branch information
ananthb committed Oct 3, 2024
0 parents commit 3c9c914
Show file tree
Hide file tree
Showing 13 changed files with 2,741 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/elm-stuff
/node_modules
/public/*
!/public/index.html
!/public/bulma.min.css
20 changes: 20 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pipeline {
agent {
docker {
image 'node:14'
}
}
stages {
stage("build") {
steps {
dir("docs/explorer") {
sh '''
yarn install --production --frozen-lockfile
yarn build
'''
archiveArtifacts artifacts: "public/*"
}
}
}
}
}
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# openapi-explorer
Open API specifications are usually stored as YAML files which are hard
to read. This app will render YAML files in a legible format.

## Develop
- nodejs runtime
- yarn package manager

Install project dependencies by running `yarn install`.

## Build
Add APIs to the explorer by copying them to the [public](public) folder and
generating an api index file.

- Copy APIs - `cp -r <api-dir>/ public/`
- Generate API Index - `tree -J -P "*.yaml" public | jq '{title: "<title>" , contents: .[0].contents}' > public/apis.json`
- `yarn build`

Replace `<api-dir>` and `<title>` in the above commands as needed.

[public](public) folder will have a static website that can be deployed to any web server.
The generated site does not depend on any external resources.

32 changes: 32 additions & 0 deletions elm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"dillonkearns/elm-markdown": "4.0.2",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0",
"krisajenkins/remotedata": "6.0.1"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/parser": "1.1.0",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"rtfeldman/elm-hex": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Elm from "./src/Main.elm";
import "rapidoc";

const serversKey = "servers";
const storedServers = window.localStorage.getItem(serversKey);
const flags = storedServers ? JSON.parse(storedServers) : null;
const program = Elm.Main.init({flags: flags});
program.ports.saveServers
.subscribe(servers => window.localStorage.setItem(serversKey, JSON.stringify(servers)));
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"devDependencies": {
"elm-format": "^0.8.3"
},
"dependencies": {
"@rollup/plugin-node-resolve": "^7.1.3",
"elm": "^0.19.1-3",
"rapidoc": "^8.0.0",
"rollup": "^2.7.6",
"rollup-plugin-elm": "^2.0.2",
"rollup-plugin-terser": "^5.3.0"
},
"name": "openapi-explorer",
"version": "1.0.0",
"main": "index.js",
"license": "UNLICENSED",
"private": true,
"scripts": {
"start": "rollup -c --watch",
"build": "rollup -c --environment TERSE"
}
}
1 change: 1 addition & 0 deletions public/bulma.min.css

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenAPI Explorer</title>
<link rel="stylesheet" href="/bulma.min.css">
<script async type="module" src="/index.js"></script>
<style>rapi-doc { min-height: 100vh; }</style>
</head>
</html>
41 changes: 41 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {terser} from "rollup-plugin-terser";
import resolve from "@rollup/plugin-node-resolve";
import elm from "rollup-plugin-elm";

const terse = Boolean(process.env.TERSE)

let plugins = [
resolve(),
elm({compiler: {debug: !terse, optimize: terse}}),
];

if (terse) {
plugins.push(
terser({
ecma: 6,
output: {comments: false},
compress: {
pure_funcs: [
"F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9",
"A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9"
],
pure_getters: true,
keep_fargs: false,
unsafe_comps: true,
unsafe: true,
passes: 2
},
mangle: true
})
)
}

export default {
input: ["index.js"],
output: {
dir: "public",
format: "esm",
sourcemap: false,
},
plugins: plugins
};
147 changes: 147 additions & 0 deletions src/Api.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
port module Api exposing
( Apis
, Data
, Entry(..)
, IndexData
, Servers
, apisDecoder
, dropExtension
, getApis
, getIndex
, reCase
, saveServers
, serversDecoder
)

import Http
import Json.Decode as Decode exposing (Decoder, field, string)
import RemoteData


dropExtension : String -> String
dropExtension =
splitExtensions >> Tuple.first


splitExtensions : String -> ( String, String )
splitExtensions path =
case String.split "." path of
[] ->
( "", "" )

[ a ] ->
( a, "" )

x :: xs ->
( x, String.join "." xs )


{-| Convert a string from dash-case to title-case.
reCase "foo-bar"
-- "Foo Bar"
-}
reCase : String -> String
reCase =
String.split "-"
>> List.map (\w -> (String.left 1 w |> String.toUpper) ++ String.dropLeft 1 w)
>> String.join " "



-- APIS


type alias Apis =
{ title : String
, contents : List Entry
}


type alias Data =
RemoteData.WebData Apis


getApis : (Data -> msg) -> Cmd msg
getApis got =
Http.get
{ url = "/apis.json"
, expect =
Http.expectJson
(RemoteData.fromResult >> got)
apisDecoder
}


apisDecoder : Decoder Apis
apisDecoder =
Decode.map2 Apis
(field "title" string)
(field "contents" (Decode.list entryDecoder))


type Entry
= Directory String (List Entry)
| File String


entryDecoder : Decoder Entry
entryDecoder =
field "type" string
|> Decode.andThen
(\typ ->
case typ of
"file" ->
Decode.map File
(field "name" string)

"directory" ->
Decode.map2 Directory
(field "name" string)
(field "contents" (Decode.list entryDecoder))

_ ->
Decode.fail <| typ ++ " is not a valid entry type"
)



-- SERVERS


type alias Servers =
List String


port saveServers : Servers -> Cmd msg


serversDecoder : Decoder Servers
serversDecoder =
Decode.list Decode.string



-- INDEX


type alias IndexData =
RemoteData.WebData String


getIndex : String -> (IndexData -> msg) -> Cmd msg
getIndex idx gotIdx =
let
indexFile =
"README.md"
in
Http.get
{ url =
if idx == "index" then
"/" ++ indexFile

else
"/" ++ idx ++ "/" ++ indexFile
, expect = Http.expectString (RemoteData.fromResult >> gotIdx)
}
Loading

0 comments on commit 3c9c914

Please sign in to comment.