Skip to content

Commit

Permalink
Merge pull request #8 from nkmr-jp/develop
Browse files Browse the repository at this point in the history
💥 Supports structure generation for each request method and request parameter.
  • Loading branch information
nkmr-jp authored Nov 23, 2023
2 parents c72cc30 + d914a1e commit 3f2756c
Show file tree
Hide file tree
Showing 22 changed files with 468 additions and 48 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ node_modules
*.go
*.json
tmp
.node-version
.node-version
tests/docs
tests/jsonplaceholder.typicode.com
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ api-to-go https://api.github.com/users/github/repos
# > Docs: https://docs.github.com/en/rest
# > Response Body:
# > - api.github.com/users/user/repos.go:1
# > - api.github.com/users/user/repos.json:1
# > - api.github.com/users/user/repos_get.go:1
# > - api.github.com/users/user/repos_get.json:1
```
Generated files and directories.
Expand All @@ -63,8 +63,8 @@ Generated files and directories.
# > └── api.github.com
# > └── users
# > └── user
# > ├── repos.go
# > └── repos.json
# > ├── repos_get.go
# > └── repos_get.json
```
Generated struct file `./api.github.com/users/user/repos.go`.
Expand All @@ -78,13 +78,13 @@ package user
import "time"
// Repos represents the response body from an HTTP request.
// ReposGet is the structure of the HTTP Response Body.
//
// Status: 200 OK
// Request: GET https://api.github.com/users/github/repos
// Format: /users/{user}/repos
// Docs: https://docs.github.com/en/rest
type Repos []struct {
type ReposGet []struct {
ID int `json:"id"`
NodeID string `json:"node_id"`
Name string `json:"name"`
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"scripts": {
"sync-version": "npm version from-git --no-git-tag-version && git add package.json && git commit -m \":bookmark: [skip ci] v$(cat package.json | jq -r .version)\"",
"fetch": "cd vendor && curl -OL https://raw.githubusercontent.com/mholt/json-to-go/master/json-to-go.js",
"setup": "yarn install && yarn fetch && npm -f link && exec $SHELL -l"
"setup": "yarn install && yarn fetch && npm -f link && exec $SHELL -l",
"test": "jest"
},
"description": "Convert REST API's JSON payload to Golang struct.",
"bin": {
Expand Down
23 changes: 11 additions & 12 deletions src/buildPath.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
const {loadYaml, loadConfig} = require("./common");
const {loadYaml, loadConfig, capitalize} = require("./common");

function buildPath(url, configFile) {
function buildPath(url, configFile, opts) {
const path = _buildPath(url, configFile)
const pathArr = path.replacedUrl.split("/")
const pkg = pathArr[pathArr.length - 2].replace(/\./g, '')
const last = pathArr[pathArr.length - 1] || "index"
const struct = _capitalize(last)
const struct = capitalize(last)
pathArr.pop()
const dir = pathArr.join("/")
let method = opts?.method.toLowerCase()

return {
path,
struct,
pkg,
dir,
jsonFilePath: `${dir}/${last}.json`,
goFilePath: `${dir}/${last}.go`,
paramJsonFilePath: `${dir}/${last}_param.json`,
paramGoFilePath: `${dir}/${last}_param.go`,
jsonFilePath: `${dir}/${last}_${method}.json`,
goFilePath: `${dir}/${last}_${method}.go`,
queryJsonFilePath: `${dir}/${last}_${method}_query.json`,
queryGoFilePath: `${dir}/${last}_${method}_query.go`,
bodyJsonFilePath: `${dir}/${last}_${method}_body.json`,
bodyGoFilePath: `${dir}/${last}_${method}_body.go`,
}
}

Expand Down Expand Up @@ -70,9 +74,4 @@ function _replacePath(pathname, format) {
}
}

function _capitalize(str) {
const lower = str.toLowerCase();
return str.charAt(0).toUpperCase() + lower.slice(1);
}

module.exports = buildPath;
18 changes: 12 additions & 6 deletions src/buildPath.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ const buildPath = require('./buildPath');
test('build path', () => {
const expected = {
"dir": "api.github.com/users/user",
"goFilePath": "api.github.com/users/user/repos.go",
"jsonFilePath": "api.github.com/users/user/repos.json",
"paramGoFilePath": "api.github.com/users/user/repos_param.go",
"paramJsonFilePath": "api.github.com/users/user/repos_param.json",
"goFilePath": "api.github.com/users/user/repos_get.go",
"jsonFilePath": "api.github.com/users/user/repos_get.json",
"queryGoFilePath": "api.github.com/users/user/repos_get_query.go",
"queryJsonFilePath": "api.github.com/users/user/repos_get_query.json",
"bodyGoFilePath": "api.github.com/users/user/repos_get_body.go",
"bodyJsonFilePath": "api.github.com/users/user/repos_get_body.json",
"path": {
"pathFormat": "/users/{user}/repos",
"pathname": "/users/github/repos",
Expand All @@ -16,9 +18,13 @@ test('build path', () => {
"pkg": "user",
"struct": "Repos"
}
let opts = {
method: "GET",
}
const received = buildPath(
new URL("https://api.github.com/users/github/repos"),
"./.api-to-go.test.yml"
"./src/.api-to-go.test.yml",
opts
)
expect(received).toEqual(expected);
});
Expand All @@ -32,7 +38,7 @@ test('build path without format setting', () => {
}
const received = buildPath(
new URL("https://api.github.com/organizations"),
"./.api-to-go.test.yml"
"./src/.api-to-go.test.yml"
)
expect(received.path).toEqual(expected);
});
5 changes: 5 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ exports.loadFile = file => {
return fs.readFileSync(file, 'utf8');
};

exports.capitalize = str => {
const lower = str.toLowerCase();
return str.charAt(0).toUpperCase() + lower.slice(1);
}

exports.isJsonString = str => {
try {
JSON.parse(str);
Expand Down
77 changes: 55 additions & 22 deletions src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const fetch = require('node-fetch');
const fs = require('fs');
const jsonToGo = require('../vendor/json-to-go.js');
const buildPath = require('./buildPath');
const {isJsonString, loadConfig, loadFile, loadJson} = require("./common");
const {isJsonString, loadConfig, loadFile, loadJson, capitalize} = require("./common");

let cliOpts

Expand All @@ -24,28 +24,38 @@ function run(urlStr, body, options) {
const apiUrl = urlStr.replace(/\/$/, '')
url = new URL(apiUrl);
cfg = loadConfig(url, cliOpts.config)
path = buildPath(url, cliOpts.config)
path = buildPath(url, cliOpts.config, opts)

console.log(`Status: ${res.status} ${res.statusText}`)
console.log(`Request: ${opts.method} ${url}`)
if (path.path.pathFormat) console.log(`Format: ${path.path.pathFormat}`)
if (cfg?.["docs"] !== undefined) console.log(`Docs: ${cfg?.["docs"].join(", ")}`)
comment = buildComment(url, path, opts.method, res)

if (opts?.body) {
const paramStruct = jsonToGo(opts?.body, path.struct + "Param");
const paramContent = buildContent(
paramStruct.go, path, buildComment(url, path, opts.method), true
)
writeParam(JSON.stringify(JSON.parse(opts?.body), null, "\t"), path, paramContent)
}

return res.json()
})
.then(json => {
const struct = jsonToGo(JSON.stringify(json), path.struct);
const content = buildContent(struct.go, path, comment)
let method = capitalize(opts?.method)
const struct = jsonToGo(JSON.stringify(json), path.struct + method);
const content = buildContent(struct.go, path, comment,"")
write(json, path, content)

if (opts?.body) {
const bodyStruct = jsonToGo(opts?.body, path.struct + method + "Body");
const bodyContent = buildContent(
bodyStruct.go, path, buildComment(url, path, opts.method), "body"
)
writeBodyParam(JSON.stringify(JSON.parse(opts?.body), null, "\t"), path, bodyContent)
}
if (url?.search) {
const queryJson = queryToJson(new URLSearchParams(url.search))
const queryStr = JSON.stringify(queryJson, null, "\t")
const queryStruct = jsonToGo(queryStr, path.struct + method + "Query");
const queryContent = buildContent(
queryStruct.go, path, buildComment(url, path, opts.method), "query"
)
writeQueryParam(queryStr, path, queryContent)
}
}, () => {
console.log()
console.log("Response Body is empty.")
Expand All @@ -69,17 +79,30 @@ function write(json, path, content) {
console.log(` - ${path.jsonFilePath}:1`)
}

function writeParam(json, path, content) {
fs.writeFile(path.paramJsonFilePath, json, (err) => {
function writeBodyParam(json, path, content) {
fs.writeFile(path.bodyJsonFilePath, json, (err) => {
if (err) throw err;
});
fs.writeFile(path.paramGoFilePath, content, (err) => {
fs.writeFile(path.bodyGoFilePath, content, (err) => {
if (err) throw err;
});
console.log()
console.log("Request Body Parameter:")
console.log(` - ${path.paramGoFilePath}:1`)
console.log(` - ${path.paramJsonFilePath}:1`)
console.log(` - ${path.bodyGoFilePath}:1`)
console.log(` - ${path.bodyJsonFilePath}:1`)
}

function writeQueryParam(json, path, content) {
fs.writeFile(path.queryJsonFilePath, json, (err) => {
if (err) throw err;
});
fs.writeFile(path.queryGoFilePath, content, (err) => {
if (err) throw err;
});
console.log()
console.log("Request Query Parameter:")
console.log(` - ${path.queryGoFilePath}:1`)
console.log(` - ${path.queryJsonFilePath}:1`)
}

function buildOpts(body, cliOpts) {
Expand Down Expand Up @@ -116,7 +139,7 @@ function buildOpts(body, cliOpts) {
return opts
}

function buildContent(go, path, comment, isParam = false) {
function buildContent(go, path, comment, paramType) {
let content = `// Generated Code But Editable.
// Format The Code with \`go fmt\` or something and edit it manually to use it.
//
Expand All @@ -127,10 +150,12 @@ function buildContent(go, path, comment, isParam = false) {
if (go.indexOf('time.') !== -1) {
content += `import "time"\n\n`
}
if (isParam) {
content += `// ${go.split(" ")[1]} is the HTTP request's body parameter.\n//`
} else {
content += `// ${go.split(" ")[1]} represents the response body from an HTTP request.\n//`
if (paramType === "body") {
content += `// ${go.split(" ")[1]} is the structure of the the HTTP Request Body Parameter.\n//`
} else if (paramType === "query") {
content += `// ${go.split(" ")[1]} is the structure of the HTTP Request Query Parameter.\n//`
}else{
content += `// ${go.split(" ")[1]} is the structure of the HTTP Response Body.\n//`
}
content += comment
content += go
Expand All @@ -154,4 +179,12 @@ function buildComment(url, path, method, res = false) {
return `${comment}\n`
}

function queryToJson(query) {
const json = {}
for (const [key, value] of query.entries()) {
json[key] = value
}
return json
}

module.exports = run;
6 changes: 6 additions & 0 deletions tests/.api-to-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"jsonplaceholder.typicode.com":
docs:
- https://jsonplaceholder.typicode.com/
format:
- /posts/{post}
- /todos/{todo}
77 changes: 77 additions & 0 deletions tests/api-to-go.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bats

load helper/helper

setup() {
if [ "$BATS_TEST_NUMBER" -eq 1 ]; then
init_doc
rm -rf ./jsonplaceholder.typicode.com
fi

BASE_HOST="jsonplaceholder.typicode.com"
BASE_URL="https://$BASE_HOST"
API_TO_GO="node ../bin/api-to-go.js"
doc "## $BATS_TEST_DESCRIPTION"
}

teardown(){
write_doc_details "api-to-go"
}

@test "Command with Path parameters" {
COMMAND="$API_TO_GO $BASE_URL/todos/1"
run eval "$COMMAND"
[ "$status" -eq 0 ]

assert_files_match /todos/todo_get.go
assert_files_match /todos/todo_get.json
}

@test "Command with Query parameters" {
QUERY='?userId=1&completed=false'
COMMAND="$API_TO_GO $BASE_URL/todos$QUERY"
run eval "$COMMAND"
[ "$status" -eq 0 ]

assert_files_match /todos_get.go
assert_files_match /todos_get.json
assert_files_match /todos_get_query.go
assert_files_match /todos_get_query.json
}

@test "Command with Body parameters" {
DATA=' {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}'
COMMAND="$API_TO_GO $BASE_URL/todos '$DATA'"
run eval "$COMMAND"
[ "$status" -eq 0 ]

assert_files_match /todos_post.go
assert_files_match /todos_post.json
assert_files_match /todos_post_body.go
assert_files_match /todos_post_body.json
}

@test "Command with custom headers" {
HEADERS='{"Content-Type": "application/json"}'
COMMAND="$API_TO_GO --headers '$HEADERS' $BASE_URL/todos/1"
run eval "$COMMAND"
[ "$status" -eq 0 ]
}

@test "Command with GET method" {
COMMAND="$API_TO_GO --method GET $BASE_URL/todos/1"
run eval "$COMMAND"
[ "$status" -eq 0 ]
}

@test "Display help message" {
COMMAND="$API_TO_GO --help"
run eval "$COMMAND"
[ "$status" -eq 0 ]
[ "${lines[0]}" = "Usage: api-to-go [options] <url> [body]" ]
}
11 changes: 11 additions & 0 deletions tests/helper/diff_helper.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash

assert_files_match() {
local generated_file="./$BASE_HOST$1"
local fixture_file="./fixture/$BASE_HOST$1"

diff_output=$(diff "$generated_file" "$fixture_file")
diff_status=$?

[ "$diff_status" -eq 0 ]
}
Loading

0 comments on commit 3f2756c

Please sign in to comment.