├── nuxt-example ├── api.yml ├── package.json ├── nuxt.config.js └── pages │ └── index.vue ├── .eslintignore ├── lerna.json ├── .gitignore ├── .eslintrc ├── packages ├── yamlful │ ├── package.json │ ├── index.mjs │ └── index.js └── yamlful-nuxt │ ├── package.json │ ├── templates │ ├── api.js │ └── plugin.js │ ├── index.mjs │ └── index.js ├── package.json └── README.md /nuxt-example/api.yml: -------------------------------------------------------------------------------- 1 | users: 2 | - method: get 3 | get: /api/users/:id 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/yamlful-nuxt/templates/api.js 2 | packages/*/node_modules 3 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.4.2" 6 | } 7 | -------------------------------------------------------------------------------- /nuxt-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "nuxt": "^2.3.2", 4 | "yamlful-nuxt": "^0.3.4" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/*/node_modules/ 3 | packages/*/*.tgz 4 | yarn-error.log 5 | yarn.lock 6 | lerna-debug.log -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "extends": "standard", 11 | "plugins": ["html", "vue"] 12 | } 13 | -------------------------------------------------------------------------------- /nuxt-example/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Thanks to Pooya Parsa for the awesome Nuxt Axios module 3 | // Note that @nuxtjs/axios is automatically required by yamlin 4 | axios: { 5 | // Thanks to Ben Howdle for the amazing API testing service 6 | baseURL: 'https://reqres.in/' 7 | }, 8 | // By default, yamlful will look for .yml files in Nuxt's srcDir 9 | modules: ['yamlful-nuxt'] 10 | } 11 | -------------------------------------------------------------------------------- /packages/yamlful/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yamlful", 3 | "version": "0.4.1", 4 | "license": "MIT", 5 | "description": "YAML RESTful API loader", 6 | "author": "Jonas Galvez", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "scripts": { 11 | "lint": "eslint --ext .js,.vue ." 12 | }, 13 | "main": "./index.js", 14 | "module": "./index.mjs", 15 | "dependencies": { 16 | "@nuxtjs/axios": "^5.3.6", 17 | "js-yaml": "^3.12.0", 18 | "serialize-javascript": "^1.6.1" 19 | }, 20 | "gitHead": "2bfc667be76b0044fa78c7e2414e2f24276788cf" 21 | } 22 | -------------------------------------------------------------------------------- /packages/yamlful-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yamlful-nuxt", 3 | "version": "0.4.2", 4 | "license": "MIT", 5 | "description": "YAML RESTful API loader for Nuxt.js", 6 | "author": "Jonas Galvez", 7 | "scripts": { 8 | "lint": "eslint --ext .js,.vue ." 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "main": "src/index.js", 14 | "module": "src/index.mjs", 15 | "dependencies": { 16 | "@nuxtjs/axios": "^5.3.6", 17 | "js-yaml": "^3.12.0", 18 | "yamlful": "^0.4.1" 19 | }, 20 | "gitHead": "2bfc667be76b0044fa78c7e2414e2f24276788cf" 21 | } 22 | -------------------------------------------------------------------------------- /packages/yamlful-nuxt/templates/api.js: -------------------------------------------------------------------------------- 1 | 2 | export default (client) => ({<% 3 | for (const resource in options.api) { %><%= `\n ${resource}` %>: { 4 | <% for (const method of options.api[resource]) { 5 | if ('raw' in method) { %> 6 | <%= method.name %>: <%= method.raw %>,<% } else { %> 7 | <%= method.name %>: (<%= method.args.length > 0 ? `${method.args}, ` : '' %>params = {}) => {<% 8 | if (method.queryParams) { %> 9 | Object.assign(params, <%= method.queryParams %>)<% } %> 10 | return client.<%= method.verb %>(<%= method.endpoint %><%= method.params %>, { params }) 11 | },<% } %><% } %> 12 | },<% } %> 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "scripts": { 4 | "lint": "eslint --ext .js,.vue packages/", 5 | "postinstall": "lerna link" 6 | }, 7 | "devDependencies": { 8 | "@ljharb/eslint-config": "^13.0.0", 9 | "babel-eslint": "^10.0.1", 10 | "eslint": "^6.1.0", 11 | "eslint-config-cssnano": "^3.1.3", 12 | "eslint-config-dev": "^2.0.0", 13 | "eslint-config-standard": "^6.2.1", 14 | "eslint-loader": "^1.6.1", 15 | "eslint-plugin-html": "^2.0.0", 16 | "eslint-plugin-promise": "^3.4.1", 17 | "eslint-plugin-standard": "^2.0.1", 18 | "eslint-plugin-vue": "^4.7.1", 19 | "lerna": "^3.4.3", 20 | "standard": "^10.0.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/yamlful-nuxt/templates/plugin.js: -------------------------------------------------------------------------------- 1 | 2 | import Vuex from 'vuex' 3 | import api from './api' 4 | 5 | // extendStore adds methods to the Vuex Store 6 | // Works for server and client-side requests 7 | const extendStore = (ctx, obj) => { 8 | Object.keys(obj).forEach((key) => { 9 | // Note that all apps that use Vuex.Store in the 10 | // same global namespace will receive these methods 11 | Vuex.Store.prototype[key] = obj[key] 12 | }) 13 | } 14 | 15 | // inject() makes a property available 16 | // in Nuxt's context and Vue pages 17 | export default (ctx, inject) => { 18 | const $api = api(ctx.$axios) 19 | extendStore(ctx, { $api }) 20 | ctx.$api = $api 21 | inject('api', $api) 22 | } 23 | -------------------------------------------------------------------------------- /packages/yamlful-nuxt/index.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import yamlful from 'yamlful' 3 | 4 | export default function (userOptions = {}) { 5 | // Apply default options 6 | userOptions = Object.assign({ 7 | srcDir: this.options.srcDir, 8 | fileName: 'api.js' 9 | }, userOptions) 10 | 11 | // Add yamlful plugin 12 | this.addPlugin({ 13 | src: resolve(__dirname, 'templates/plugin.js'), 14 | fileName: 'yamlful/plugin.js' 15 | }) 16 | 17 | // Add yamlful API template 18 | this.addTemplate({ 19 | src: resolve(__dirname, 'templates/api.js'), 20 | fileName: 'yamlful/api.js', 21 | options: { api: yamlful(userOptions.srcDir) } 22 | }) 23 | 24 | this.requireModule('@nuxtjs/axios') 25 | } 26 | -------------------------------------------------------------------------------- /packages/yamlful-nuxt/index.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const yamlful = require('yamlful') 3 | 4 | module.exports = function (userOptions = {}) { 5 | // Apply default options 6 | userOptions = Object.assign({ 7 | srcDir: this.options.srcDir, 8 | fileName: 'api.js' 9 | }, userOptions) 10 | 11 | // Add yamlful plugin 12 | this.addPlugin({ 13 | src: resolve(__dirname, 'templates/plugin.js'), 14 | fileName: 'yamlful/plugin.js' 15 | }) 16 | 17 | // Add yamlful API template 18 | this.addTemplate({ 19 | src: resolve(__dirname, 'templates/api.js'), 20 | fileName: 'yamlful/api.js', 21 | options: { api: yamlful(userOptions.srcDir) } 22 | }) 23 | 24 | this.requireModule('@nuxtjs/axios') 25 | } 26 | -------------------------------------------------------------------------------- /nuxt-example/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 39 | -------------------------------------------------------------------------------- /packages/yamlful/index.mjs: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync } from 'fs' 2 | import { join } from 'path' 3 | import yaml from 'js-yaml' 4 | import serialize from 'serialize-javascript' 5 | 6 | function loadYAML (yamlFile) { 7 | if (existsSync(yamlFile)) { 8 | return yaml.safeLoad(readFileSync(yamlFile, 'utf8')) 9 | } 10 | } 11 | 12 | function loadResources (srcDir) { 13 | const resourcesPath = join(srcDir) 14 | if (!existsSync(resourcesPath)) { 15 | throw new Error(`No resources found.`) 16 | } 17 | const api = readdirSync(resourcesPath) 18 | .filter((file) => file.match(/\.ya?ml$/)) 19 | .reduce((obj, file) => { 20 | const yamlConfig = loadYAML(join(resourcesPath, file)) 21 | for (const resource in yamlConfig) { 22 | if (obj[resource]) { 23 | obj[resource].push(...yamlConfig[resource]) 24 | } else { 25 | obj[resource] = yamlConfig[resource] 26 | } 27 | } 28 | return obj 29 | }, {}) 30 | return api 31 | } 32 | 33 | function formatRaw (raw) { 34 | raw = raw.trim().split(/\r?\n/) 35 | for (let i = 1; i < raw.length; i++) { 36 | raw[i] = ` ${raw[i]}` 37 | } 38 | return raw.join('\n') 39 | } 40 | 41 | function generateMethod (methodConfig) { 42 | const keys = Object.keys(methodConfig) 43 | const verb = ['get', 'post', 'put', 'delete'] 44 | .find((verb) => keys.includes(verb)) 45 | const args = [] 46 | const params = [] 47 | if (verb in methodConfig) { 48 | let endpoint = methodConfig[verb] 49 | .replace(/\:([\w\d_\$]+)/g, (_, param) => { 50 | args.push(param) 51 | return `\${${param}}` 52 | }) 53 | if (endpoint.includes('${')) { 54 | endpoint = `\`${endpoint}\`` 55 | } else { 56 | endpoint = `'${endpoint}'` 57 | } 58 | if (['post', 'put'].includes(verb)) { 59 | args.push('payload') 60 | params.push('payload') 61 | } 62 | const queryParams = methodConfig.params 63 | ? serialize(methodConfig.params, {spaces: 2}) 64 | : undefined 65 | return { 66 | verb, 67 | endpoint, 68 | queryParams, 69 | name: methodConfig.method, 70 | args: args.join(', '), 71 | params: params.length > 0 72 | ? `, ${params.join(', ')}` 73 | : '' 74 | } 75 | } else if ('raw' in methodConfig) { 76 | return { 77 | name: methodConfig.method, 78 | raw: formatRaw(methodConfig.raw) 79 | } 80 | } else { 81 | throw new Error(`Method configuration invalid: ${ 82 | JSON.stringify(methodConfig, null, 2) 83 | }`) 84 | } 85 | } 86 | 87 | export default function yamlfu (srcDir) { 88 | const api = loadResources(srcDir) 89 | const tree = {} 90 | for (const resource in api) { 91 | tree[resource] = api[resource].map(generateMethod) 92 | } 93 | return tree 94 | } 95 | -------------------------------------------------------------------------------- /packages/yamlful/index.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readdirSync, readFileSync } = require('fs') 2 | const { join } = require('path') 3 | const yaml = require('js-yaml') 4 | const serialize = require('serialize-javascript') 5 | 6 | function loadYAML (yamlFile) { 7 | if (existsSync(yamlFile)) { 8 | return yaml.safeLoad(readFileSync(yamlFile, 'utf8')) 9 | } 10 | } 11 | 12 | function loadResources (srcDir) { 13 | const resourcesPath = join(srcDir) 14 | if (!existsSync(resourcesPath)) { 15 | throw new Error(`No resources found.`) 16 | } 17 | const api = readdirSync(resourcesPath) 18 | .filter((file) => file.match(/\.ya?ml$/)) 19 | .reduce((obj, file) => { 20 | const yamlConfig = loadYAML(join(resourcesPath, file)) 21 | for (const resource in yamlConfig) { 22 | if (obj[resource]) { 23 | obj[resource].push(...yamlConfig[resource]) 24 | } else { 25 | obj[resource] = yamlConfig[resource] 26 | } 27 | } 28 | return obj 29 | }, {}) 30 | return api 31 | } 32 | 33 | function formatRaw (raw) { 34 | raw = raw.trim().split(/\r?\n/) 35 | for (let i = 1; i < raw.length; i++) { 36 | raw[i] = ` ${raw[i]}` 37 | } 38 | return raw.join('\n') 39 | } 40 | 41 | function generateMethod (methodConfig) { 42 | const keys = Object.keys(methodConfig) 43 | const verb = ['get', 'post', 'put', 'delete'] 44 | .find((verb) => keys.includes(verb)) 45 | const args = [] 46 | const params = [] 47 | if (verb in methodConfig) { 48 | let endpoint = methodConfig[verb] 49 | .replace(/:([\w\d_$]+)/g, (_, param) => { 50 | args.push(param) 51 | return `\${${param}}` 52 | }) 53 | if (endpoint.includes('${')) { 54 | endpoint = `\`${endpoint}\`` 55 | } else { 56 | endpoint = `'${endpoint}'` 57 | } 58 | if (['post', 'put'].includes(verb)) { 59 | args.push('payload') 60 | params.push('payload') 61 | } 62 | const queryParams = methodConfig.params 63 | ? serialize(methodConfig.params, {spaces: 2}) 64 | : undefined 65 | return { 66 | verb, 67 | endpoint, 68 | queryParams, 69 | name: methodConfig.method, 70 | args: args.join(', '), 71 | params: params.length > 0 72 | ? `, ${params.join(', ')}` 73 | : '' 74 | } 75 | } else if ('raw' in methodConfig) { 76 | return { 77 | name: methodConfig.method, 78 | raw: formatRaw(methodConfig.raw) 79 | } 80 | } else { 81 | throw new Error(`Method configuration invalid: ${ 82 | JSON.stringify(methodConfig, null, 2) 83 | }`) 84 | } 85 | } 86 | 87 | module.exports = function yamlfu (srcDir) { 88 | const api = loadResources(srcDir) 89 | const tree = {} 90 | for (const resource in api) { 91 | tree[resource] = api[resource].map(generateMethod) 92 | } 93 | return tree 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is no longer maintained. 2 | 3 | Using YAML to define code snippets is a terrible idea. I don't know what I was thinking. 4 | 5 | Check out [`fastify-vite`](https://github.com/galvez/fastify-vite) and [`fastify-api`](https://github.com/galvez/fastify-api) for a better path forward. 6 | 7 | API clients should be autogenerated from the server code that implements them. 8 | 9 | *** 10 | 11 | **yamlful** is a utility for **HTTP client code generation** from YAML: 12 | 13 | ```yaml 14 | sample: 15 | - method: get 16 | get: /resource/:id/subresource/:subId 17 | - method: create 18 | post: /resource/:id/subresource 19 | - method: update 20 | put: /resource/:id/subresource/:subId 21 | - method: remove 22 | delete: myresource/ 23 | ``` 24 | 25 | It uses a simple pattern to determine _function arguments_ and _HTTP parameters_, so that methods that use `PUT` or `POST` get a payload and others don't, while preserving the URL parameters in each YAML-defined endpoint. 26 | 27 | The above YAML file can be used to generate a snippet like this: 28 | 29 | ```js 30 | const sample = { 31 | get: (id, subId, params = {}) => { 32 | return client.get(`/resource/${id}/subresource/${subId}`, { params }) 33 | }, 34 | create: (id, payload, params = {}) { 35 | return client.post(`/resource/${id}/subresource`, payload, { params }) 36 | }, 37 | update: (id, subId, payload, params = {}) { 38 | return client.put(`/resource/${id}/subresource/${subId}`, payload, { params }) 39 | }, 40 | remove: (id, params = {}) => { 41 | return client.delete(`myresource/${id}`, { params }) 42 | } 43 | } 44 | ``` 45 | 46 | # Motivation 47 | 48 | Boilerplate JavaScript for exposing HTTP API client methods is pretty simple most 49 | of the time. 50 | 51 | However, when you have a huge API with dozens of different resources, more 52 | streamlined YAML configuration makes it easier to maintain it while dealing with 53 | constant change. **yamlful** was born by identifying these key simple patterns 54 | when connecting JavaScrit methods to JSON HTTP APIs. 55 | 56 | # Nuxt.js module 57 | 58 | Bundled in this repository is a [Nuxt.js][1] module (`yamlful-nuxt`) that uses 59 | `yamlful` to generate similar code, integrating itself to [@nuxtjs/axios][2] 60 | and exposing API methods to Vue pages. 61 | 62 | ```sh 63 | npm install yamlful-nuxt --save 64 | ``` 65 | 66 | In `nuxt.config.js`: 67 | 68 | ```js 69 | export default { 70 | // Thanks to Pooya Parsa for the awesome Nuxt Axios module 71 | // Note that @nuxtjs/axios is automatically required by yamlin 72 | axios: { 73 | // Thanks to Ben Howdle for the amazing API testing service 74 | baseURL: 'https://reqres.in/' 75 | }, 76 | // By default, yamlful will look for .yml files in Nuxt's srcDir 77 | modules: ['yamlful-nuxt'] 78 | } 79 | ``` 80 | 81 | In `pages/index.vue`: 82 | 83 | ```js 84 | export default { 85 | async asyncData ({ $api }) { 86 | // $api available in SSR context 87 | const response = await $api.users.get(1) 88 | return { 89 | user: response.data 90 | } 91 | }, 92 | data: () => ({ 93 | user: {} 94 | }), 95 | methods: { 96 | async loadTwo() { 97 | // this.$api available in the client context 98 | const response = await this.$api.users.get(2) 99 | this.user = response.data 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | ## Raw methods 106 | 107 | The Nuxt.js module also allows you to inline JavaScript in YAML for defining raw methods: 108 | 109 | **Input**: 110 | 111 | ``` 112 | - method: custom 113 | raw: | 114 | (customParam) => { 115 | client.get(`/customresource/${customParam}`) 116 | } 117 | ``` 118 | 119 | **Output**: 120 | 121 | ``` 122 | custom: (customParam) => { 123 | client.get(`/customresource/${customParam}`) 124 | } 125 | ``` 126 | 127 | Note that `client` is used to [inject][3] Nuxt's axios instance. 128 | 129 | See the [API template][4] used for the Nuxt module. 130 | 131 | # Other frameworks 132 | 133 | Modules and extensions for other frameworks can be implemented using the main exported function in the `yamlful` package. PRs are very much welcome. 134 | 135 | [1]: https://nuxtjs.org 136 | [2]: https://github.com/nuxt-community/axios-module 137 | [3]: https://blog.lichter.io/posts/organize-and-decouple-your-api-calls-in-nuxtjs 138 | [4]: https://github.com/galvez/yamlful/blob/master/packages/yamlful-nuxt/templates/api.js 139 | --------------------------------------------------------------------------------