Repository: Amareis/another-rest-client
Branch: master
Commit: 95b51e79c430
Files: 11
Total size: 36.8 KB
Directory structure:
gitextract_hpts000u/
├── .gitignore
├── LICENSE
├── README.adoc
├── build.js
├── examples/
│ ├── github.html
│ └── github.ts
├── package.json
├── src/
│ ├── minivents.ts
│ └── rest-client.ts
├── test/
│ └── test.js
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
bower_components
node_modules
.idea
npm-debug.log
dist
yarn-error.log
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Amareis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.adoc
================================================
= another-rest-client
Simple REST API client that makes your code lesser and more beautiful than without it.
There is some rest clients - https://github.com/marmelab/restful.js[restful.js], https://github.com/cujojs/rest[cujojs/rest] or https://github.com/lincolnloop/amygdala[amygdala] - so why you need another rest client? First, because all of this is not maintained anymore :) But also, because with it your code less and more beautiful than without it or with any analogs. Also, its code really simple - less than 300 sloc and (almost) without magic, so you can just read it (and fix, may be?) if something go wrong.
To prove my words, here is an minimal working code (you can explore more examples https://github.com/Amareis/another-rest-client/tree/master/examples[here]):
And it works with typescript!
[source,typescript]
----
import {RestClient} from 'another-rest-client'
const api = new RestClient('https://api.github.com').withRes({
repos: 'releases',
} as const)
api.repos('Amareis/another-rest-client').releases('latest').get().then((release: any) => {
console.log(release)
document.write('Latest release of another-rest-client:<br>')
document.write('Published at: ' + release.published_at + '<br>')
document.write('Tag: ' + release.tag_name + '<br>')
})
----
== Installation
Library is available with npm:
[source,shell]
----
npm install another-rest-client
# or
yarn add another-rest-client
----
Now, add it in script tag or require it or import it:
[source,js]
----
const {RestClient} = require('another-rest-client')
import {RestClient} from 'another-rest-client'
----
*ATTENTION:* If you want to use another-rest-client with node.js, you must define XMLHttpRequest before import (https://github.com/driverdan/node-XMLHttpRequest[see here]):
[source,js]
----
global.XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest
----
== Usage
[source,js]
----
const api = new RestClient('https://example.com')
----
And here we go! First, let's define resources, using `res` method:
[source,js]
----
api.res('cookies') //it gets resource name and returns resource
api.res(['cows', 'bees']) //or it gets array of resource names and returns array of resources
api.res({ //or it gets object and returns object where resource is available by name
dogs: [
'toys',
'friends'],
cats: 0,
humans:
'posts',
})
/* last string is equal to:
api.res('dogs').res(['toys', 'friends'])
api.res('cats')
api.res('humans').res('posts') */
----
Now we can query our resources using methods `get` (optionally gets query args), `post`, `put`, `patch` (gets body content) and `delete`. All these methods returns promise, that resolves with object that given by server or rejects with `XMLHttpRequest` instance:
[source,js]
----
api.cookies.get() //GET https://example.com/cookies
api.cookies.get({fresh: true}) //GET https://example.com/cookies?fresh=true
api.cookies.get({'filter[]': 'fresh'}, {'filter[]': 'taste'}) //GET https://example.com/cookies?filter%5B%5D=fresh&filter%5B%5D=taste
//POST https://example.com/cows, body="{"color":"white","name":"Moo"}"
api.cows.post({color: 'white', name: 'Moo'}).then((cow) => {
console.log(cow) //just object, i.e. {id: 123, name: 'Moo', color: 'white'}
}, (xhr) => {
console.log(xhr) //XMLHtppRequest instance
})
----
If you want query single resource instance, just pass it id into resource:
[source,js]
----
api.cookies(42).get() //GET https://example.com/cookies/42
//GET https://example.com/cookies/42?fields=ingridients,baker
api.cookies(42).get({fields: ['ingridients', 'baker']})
api.bees(12).put({state: 'dead'}) //PUT https://example.com/bees/12, body="{"state":"dead"}"
api.cats(64).patch({age: 3}) //PATCH https://example.com/cats/64, body="{"age":3}"
----
You can query subresources easily:
[source,js]
----
api.dogs(1337).toys.get() //GET https://example.com/dogs/1337/toys
api.dogs(1337).friends(2).delete() //DELETE https://example.com/dogs/1337/friends/2
//POST https://example.com/humans/me/posts, body="{"site":"habrahabr.ru","nick":"Amareis"}"
api.humans('me').posts.post({site: 'habrahabr.ru', nick: 'Amareis'})
----
You can use `url` resource method to get resource url:
[source,js]
----
api.dogs.url() === '/dogs'
api.dogs(1337).friends(1).url() === '/dogs/1337/friends/1'
----
And, of course, you always can use ES6 async/await to make your code more readable:
[source,js]
----
const me = api.humans('me')
const i = await me.get()
console.log(i) //just object, i.e. {id: 1, name: 'Amareis', profession: 'programmer'}
const post = await me.posts.post({site: 'habrahabr.ru', nick: i.name})
console.log(post) //object
----
== TypeScript
Library infer types from schema, passed to `res`. But it returns new resource (or array or object), so to use it
correctly, you need to use `withRes` method, which returns modified original resource:
[source,typescript]
----
let api = new RestClient('https://api.github.com').withRes({
repos: 'releases',
} as const) // as const needed to infer resources names
// correctly infer all this subresources!
api.repos('Amareis/another-rest-client').releases('latest').get()
----
You can then add more resources reusing already typed resource:
[source,typescript]
----
api = api.withRes('additional-resource')
----
**Custom shortcuts currently not working with TypeScript! And shorcuts always will be in typings, even if they are disabled.**
== Events
`RestClient` use https://github.com/allouis/minivents[minivents] and emit some events:
- `request` - when `open` XMLHttpRequest, but before `send`.
- `response` - when get server response.
- `success` - when get server response with status 200, 201 or 204.
- `error` - when get server response with another status.
All events gets current XMLHttpRequest instance.
Often use case - authorization:
[source,js]
----
api.on('request', xhr => {
xhr.setRequestHeader('Authorization', 'Bearer xxxTOKENxxx')
})
----
Also, returns by `get`, `post`, `put`, `patch` and `delete` `Promise` objects also emit these events, but only for current request.
[source,js]
----
api.dogs(1337).toys.get().on('success', console.log.bind(console)).then(toys => "...") //in log will be xhr instance
api.dogs(1337).toys.get().then(toys => "...") //log is clear
----
You can use events to set `responseType` XMLHttpRequest property, to handle binary files (and you can compose it with custom decoders, as described below, to automatically convert blob to File object):
[source,js]
----
api.files('presentation.pdf').get().on('request', xhr => xhr.responseType = 'blob').then(blobObj => "...")
----
== Configuration
All the examples given above are based on the default settings. If for some reason you are not satisfied, read this section.
All configuration is done using the object passed to the constructor or method `conf`. Some options are also duplicated by optional methods arguments.
`conf` returns full options. If you call it without parameters (just `conf()`), it gives you current options.
[source,js]
----
console.log(api.conf())
/* Defaults:
{
"trailing": "",
"shortcut": true,
"shortcutRules": [],
"contentType": "application/json",
"encodings": {
"application/x-www-form-urlencoded": {encode: encodeUrl},
"application/json": {encode: JSON.stringify, decode: JSON.parse}
}
}*/
----
If you want change RestClient host (lol why?..), you can just:
[source,js]
----
api.host = 'https://example2.com'
----
=== Trailing symbol
Some APIs require trailing slash (for example, this is the default behavior in the django-rest-framework). By default another-rest-client doesn't use any trailing symbol, but you can change this:
[source,js]
----
const api = new RestClient('https://example.com', {trailing: '/'})
//or
api.conf({trailing: '/'})
----
Of course, you can pass all you want (`{trailing: '/i-have-no-idea-why-you-want-this-but-you-can/'}`).
=== Shortcuts
Shortcuts - resources and subresources, that accessible as parent resource field:
[source,js]
----
api.cars === undefined
const cars = api.res('cars')
api.cars === cars //api.cars is shortcut for 'cars' resource
----
By default, another-rest-client will make shortcuts for defined resources. This behavior can be disabled in three ways:
[source,js]
----
api.sounds === undefined
//first way
const api = new RestClient('https://example.com', {shortcut: false})
//or, second way
api.conf({shortcut: false})
//or, third way
const sounds = api.res('sounds', false)
//and, still...
api.sounds === undefined
----
First two ways disables shortcuts globally - on all resources and subresources. Third way disables shortcuts locally - in one `res` call. Also, with third way you can locally _enable_ shortcuts (pass `true` as second `res` argument) when globally they are disabled.
Local disable of shortcuts can solve some name conflicts (when resource shortcut overwrites some method), but, probably, you will not be affected by this.
*It is strongly recommended do not disable the shortcuts, they greatly enhance code readability.*
You can also add custom shortcuts for resources via rules. Those can be configured via the `shortcutRules` array in the options. When a resource is added all rules will be invoked with the resource name as argument. If the return value is a non-empty string, it will serve as an additional shortcut.
Have a look at this example which will convert strings with dashes into their camel-case counterpart to serve as additional shortcut:
[source,js]
----
const DASH_REG = /(-)(.)/g
function dashReplace(resourceName) {
return resourceName.replace(DASH_REG, (match, p1, p2) => p2.toUpperCase())
}
const api = new RestClient('https://example.com', {shortcutRules: [ dashReplace ]})
api.res('engine-rest')
api['engine-rest'] // standard shortcut
api.engineRest // custom shortcut to improve readability
----
=== Request content type
When you call `post`, `put` or `patch`, you pass an object to be encoded into string and sent to the server. But how it will be encoded and what `Content-Type` header will be set?
By default - in json (`application/json`), using `JSON.stringify`. To change this behavior, you can manually set request content type:
[source,js]
----
const api = new RestClient('https://example.com', {contentType: 'application/x-www-form-urlencoded'})
//or by conf
api.conf({contentType: 'application/x-www-form-urlencoded'})
//or by second argument in 'post', 'put' or 'patch'
api.cookies.post({fresh: true}, 'application/x-www-form-urlencoded')
----
By default RestClient can encode data in `application/json` and `application/x-www-form-urlencoded`. You can add (or replace defaults with) your own encoders:
[source,js]
----
const opts = {
contentType: 'application/x-my-cool-mime',
encodings: {
'application/x-my-cool-mime': {
encode: (objectPassedToPostPutOrPatch) => {
//...
return encodedToStringObject
}
}
}
}
const api = new RestClient('https://example.com', opts)
//or by conf
api.conf(opts)
----
If there is no suitable encoder, passed object will be passed to the XMLHttpRequest.send without changes.
=== Response content type
When server answers, it give `Content-Type` header. another-rest-client smart enough to parse it and decode `XMLHttpRequest.responseText` into object. By default it can decode only `application/json` using `JSON.parse`, but you can add your own decoders:
[source,js]
----
const opts = {
encodings: {
'application/x-my-cool-mime': {
decode: (stringFromXhrResponseText) => {
//...
return decodedFromStringObject
}
}
}
}
const api = new RestClient('https://example.com', opts)
//or by conf
api.conf(opts)
----
If there is no suitable decoder (or server given't `Content-Type` header), gotten `XMLHttpRequest.response` will be passed to Promise.resolve without changes.
Of course, you can combine encoders and decoders for single MIME:
[source,js]
----
const opts = {
contentType: 'application/x-my-cool-mime',
encodings: {
'application/x-my-cool-mime': {
encode: (objectPassedToPostPutOrPatch) => {
//...
return encodedToStringObject
},
decode: (stringFromXhrResponseText) => {
//...
return decodedFromStringObject
}
}
}
}
const api = new RestClient('https://example.com', opts)
//or by conf
api.conf(opts)
----
== Contributing
That's easy:
[source,bash]
----
git clone https://github.com/Amareis/another-rest-client.git
cd another-rest-client
yarn
echo "//Some changes..." >> src/rest-client.ts
yarn build && yarn test
----
================================================
FILE: build.js
================================================
const path = require("path");
const esbuild = require("esbuild");
let outdir = path.resolve(process.cwd(), "dist");
const globalName = 'RestClient';
// esbuild doesn't support umd, here's a hack from
// https://github.com/evanw/esbuild/pull/1331#issuecomment-887877002
let footer = `(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
Object.defineProperty(exports, "__esModule", {
value: true
});
} else {
root.${globalName} = factory().${globalName};
}
}(typeof self !== 'undefined' ? self : this, () => ${globalName}));`;
let files = {
'rest-client.js': './src/rest-client.js',
'rest-client.min.js': './src/rest-client.js',
}
let builds = Object.entries(files)
.map(([filename, source]) => {
return {
entryPoints: [source],
sourcemap: true,
outfile: path.resolve(outdir, filename),
bundle: true,
platform: "browser",
format: "iife",
footer: {
js: footer,
},
globalName,
minify: /\.min\.m?js$/.test(filename)
}
})
Promise.all(builds.map(esbuild.build));
================================================
FILE: examples/github.html
================================================
<html lang="en">
<head>
<title>another-rest-client</title>
<script src="../dist/rest-client.js"></script>
<script>
window.onload = function () {
const api = new RestClient('https://api.github.com')
api.res({repos: 'releases'})
window.api = api
api.repos('Amareis/another-rest-client').releases('latest').get().then((release) => {
console.log(release)
document.body.innerHTML =
'Latest release of another-rest-client:<br>' +
`Published at: ${release.published_at}<br>` +
`Tag: ${release.tag_name}<br>`
})
}
</script>
</head>
<body>
</body>
</html>
================================================
FILE: examples/github.ts
================================================
import RestClient from '../src/rest-client'
const api = new RestClient('https://api.github.com').withRes({
repos: 'releases',
} as const)
api.repos('Amareis/another-rest-client').releases('latest').get().then((release: any) => {
console.log(release)
document.body.innerHTML =
'Latest release of another-rest-client:<br>' +
`Published at: ${release.published_at}<br>` +
`Tag: ${release.tag_name}<br>`
})
================================================
FILE: package.json
================================================
{
"name": "another-rest-client",
"version": "0.7.0",
"description": "Simple REST API client that makes your code lesser and more beautiful than without it.",
"main": "dist/rest-client.js",
"types": "dist/rest-clients.d.ts",
"scripts": {
"test": "mocha",
"build": "tsc && node ./build.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Amareis/another-rest-client.git"
},
"keywords": [
"rest"
],
"author": "joe <terma95@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Amareis/another-rest-client/issues"
},
"homepage": "https://github.com/Amareis/another-rest-client#readme",
"devDependencies": {
"chai": "^4.3.6",
"esbuild": "^0.14.25",
"form-data": "^4.0.0",
"mocha": "^9.2.1",
"sinon": "^13.0.1",
"typescript": "^4.6.2"
}
}
================================================
FILE: src/minivents.ts
================================================
export type Listener = (...p: any[]) => void
export type Target = {
on: (type: string, func: Listener, ctx: any) => Target
off: (type?: string, func?: Listener) => Target
emit: (type: string, ...args: any[]) => Target
}
export function Events<T>(target: T): T & Target {
const t: T & Target = target as any
let events: Record<string, [Listener, any]> = {}
/**
* On: listen to events
*/
t.on = function(type, func, ctx) {
(events[type] = events[type] || []).push([func, ctx])
return t
}
/**
* Off: stop listening to event / specific callback
*/
t.off = function(type, func) {
if (!type) events = {}
const list = events[type as any] || []
let i = func ? list.length : 0
while (i--) func == list[i][0] && list.splice(i, 1)
return t
}
/**
* Emit: send event, callbacks will be triggered
*/
t.emit = function(type: string, ...args: any[]){
const e = events[type] || [], list = e.length > 0 ? e.slice(0, e.length) : e
let i = 0, j
while (j = list[i++]) j[0].apply(j[1], args)
return t
}
return t
}
================================================
FILE: src/rest-client.ts
================================================
import {Events, Target} from './minivents.js'
function entries<T extends Record<string, any>>(obj: T): ([Extract<keyof T, string>, any])[] {
return Object.entries(obj) as any
}
type Arg = Record<string, string | boolean | number>
function encodeUrl(data: Arg) {
let res = ''
for (let [k, v] of entries(data))
res += encodeURIComponent(k) + '=' + encodeURIComponent(v) + '&'
return res.slice(0, res.length - 1)
}
function safe(func: Function, data: any) {
try {
return func(data)
}
catch(e) {
console.error('Error in function "' + func.name + '" while decode/encode data')
console.log(func)
console.log(data)
console.log(e)
return data
}
}
export type CustomShortcut = (resName: string) => string
export type Encodings = {
[mime: string]: {
encode?: (data: any) => string
decode?: (xhrResponse: string) => any
}
}
export type Opts = {
trailing: string
shortcut: boolean
shortcutRules: CustomShortcut[]
contentType: string
encodings: Encodings
}
type Ress = string | readonly string[] | { [resName: string]: Ress | 0 | null | undefined}
type MakeRes<T extends Ress> =
T extends string ? {[resName in T]: Res}
: T extends ReadonlyArray<infer S> ? Res & ([S] extends [string] ? {[resName in S]: Res} : never)
: {[ResName in keyof T]: T[ResName] extends Ress ? Res & MakeRes<T[ResName]> : Res}
type MR<T extends Ress> =
T extends string ? Res
: T extends string[] ? Res[]
: {[ResName in keyof T]: T[ResName] extends Ress ? Res & MR<T[ResName]> : Res}
interface Res {
(id?: string | number): this
}
class Res extends Function {
private _shortcuts: Record<string, Res> = {}
private _resources: Record<string, Res> = {}
private get _client() {
return this._c()
}
constructor(private _c: () => RestClient, private _parent: Res | undefined, private _name: string, private _id?: string) {
super('id', 'return arguments.callee.__call(id)')
}
private __call = (newId?: string) => {
if (newId === undefined)
return this
return this._clone(this._parent, newId)
}
private _clone = (parent: Res | undefined, newId?: string) => {
let copy = new Res(this._c, parent, this._name, newId)
copy._shortcuts = this._shortcuts
for (let resName in this._resources) {
copy._resources[resName] = this._resources[resName]._clone(copy)
if (resName in copy._shortcuts)
(copy as any)[resName] = copy._resources[resName]
}
return copy
}
withRes = <T extends Ress>(resources: T, shortcut=this._client._opts.shortcut): this & MakeRes<T> => {
this.res(resources, shortcut)
return this as any
}
res = <T extends Ress>(resources: T, shortcut=this._client._opts.shortcut): MR<T> => {
let makeRes = (resName: string) => {
if (resName in this._resources)
return this._resources[resName]
let r = new Res(this._c, this, resName)
this._resources[resName] = r
if (shortcut) {
const self = this as any as MakeRes<T>
this._shortcuts[resName] = r
self[resName] = r
for (const rule of this._client._opts.shortcutRules) {
const customShortcut = rule(resName)
if (customShortcut && typeof customShortcut === 'string') {
this._shortcuts[customShortcut] = r
self[customShortcut] = r
}
}
}
return r
}
// (resources instanceof String) don't work in js.
if (typeof resources === 'string')
return makeRes(resources) as any
if (resources instanceof Array)
return resources.map(makeRes) as any
if (resources instanceof Object) {
let resObj: Record<string, Res> = {}
for (let resName in resources) {
let r = makeRes(resName)
const nr = resources[resName]
if (nr) {
r.res(nr as any)
}
resObj[resName] = r
}
return resObj as any
}
throw new TypeError('Wrong "resources" argument! Should be string, array of strings or object')
}
url = (): string => {
let url = this._parent?.url() ?? ''
if (this._name)
url += '/' + this._name
if (this._id !== undefined)
url += '/' + this._id
return url
}
get = (...args: Arg[]) => {
let url = this.url()
const query = args.map(encodeUrl).join('&')
if (query)
url += '?' + query
return this._client._request('GET', url)
}
post = (data: any, contentType = this._client._opts.contentType) => {
return this._client._request('POST', this.url(), data, contentType)
}
put = (data: any, contentType = this._client._opts.contentType) => {
return this._client._request('PUT', this.url(), data, contentType)
}
patch = (data: any, contentType = this._client._opts.contentType) => {
return this._client._request('PATCH', this.url(), data, contentType)
}
delete = () => {
return this._client._request('DELETE', this.url())
}
}
export class RestClient extends Res implements Target {
host: string
_opts: Opts = {
trailing: '',
shortcut: true,
shortcutRules: [],
contentType: 'application/json',
encodings: {
'application/x-www-form-urlencoded': {encode: encodeUrl},
'application/json': {encode: JSON.stringify, decode: JSON.parse},
}
}
emit!: Target['emit']
on!: Target['on']
off!: Target['off']
constructor(host: string, options?: Partial<Opts> & Encodings) {
super(() => this, undefined, '', undefined)
Events(this)
this.host = host
this.conf(options)
}
conf(options: Partial<Opts> = {}): Opts {
for (const [k, v] of entries(options)) {
if (k === 'encodings') {
Object.assign(this._opts.encodings, v)
continue
}
if (k in this._opts) {
(this._opts as any)[k] = v
} else {
this._opts.encodings[k] = v
console.warn(`There is no option '${k}' in another-rest-client options. Probably this is encoding and should be in 'encodings' option!`)
}
}
return {
...this._opts,
encodings: {...this._opts.encodings},
shortcutRules: [...this._opts.shortcutRules],
}
}
_request(method: string, url: string, data: any = null, contentType: string | null = null) {
if (url.indexOf('?') === -1)
url += this._opts.trailing
else
url = url.replace('?', this._opts.trailing + '?')
let xhr = new XMLHttpRequest()
xhr.open(method, this.host + url, true)
if (contentType) {
let mime = this._opts.encodings[contentType]
if (mime && mime.encode)
data = safe(mime.encode, data)
if (!(contentType === 'multipart/form-data' && data instanceof FormData))
xhr.setRequestHeader('Content-Type', contentType)
}
let p = Events(new Promise((resolve, reject) =>
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
this.emit('response', xhr)
p.emit('response', xhr)
if (xhr.status === 200 || xhr.status === 201 || xhr.status === 204) {
this.emit('success', xhr)
p.emit('success', xhr)
let res = xhr.response
let responseHeader = xhr.getResponseHeader('Content-Type')
if (responseHeader) {
let responseContentType = responseHeader.split(';')[0]
let mime = this._opts.encodings[responseContentType]
if (mime && mime.decode)
res = safe(mime.decode, res)
}
p.off()
resolve(res)
} else {
this.emit('error', xhr)
p.emit('error', xhr)
p.off()
reject(xhr)
}
}
}
))
Promise.resolve().then(() => {
this.emit('request', xhr)
p.emit('request', xhr)
xhr.send(data)
})
return p
}
}
export default RestClient
================================================
FILE: test/test.js
================================================
require('chai').should();
const sinon = require('sinon');
const FormData = require('form-data');
const {RestClient} = require('../dist/rest-client');
const host = 'https://example.com';
xhr = global.XMLHttpRequest = sinon.useFakeXMLHttpRequest();
global.FormData = FormData;
describe('RestClient', () => {
describe('#_request()', () => {
let api;
beforeEach(() => {
api = new RestClient(host);
api.res('cookies');
});
it('should append trailing symbol which passed to constructor', () => {
let req;
xhr.onCreate = r => req = r;
new RestClient(host, {trailing: '/'}).res('cookies').get();
req.url.should.be.equal(host + '/cookies/');
});
it('should append trailing symbol before args', () => {
let req;
xhr.onCreate = r => req = r;
new RestClient(host, {trailing: '/'}).res('cookies').get({fresh: true});
req.url.should.be.equal(host + '/cookies/?fresh=true');
});
it('should emit events', (done) => {
let req, bool;
xhr.onCreate = r => req = r;
const p = api.on('request', xhr => bool = true).cookies.get({fresh: true});
req.url.should.be.equal(host + '/cookies?fresh=true');
setTimeout(() => req.respond(200, [], '{a:1}'), 0);
p.then(() => {
bool.should.be.equal(true)
done();
}).catch(done);
});
it('should correct handle form data', () => {
let req;
xhr.onCreate = r => req = r;
const p = api.cookies.post(new FormData(), 'multipart/form-data');
req.url.should.be.equal(host + '/cookies');
(typeof req.requestHeaders['Content-Type']).should.be.equal('undefined');
});
});
});
describe('resource', () => {
describe('#res()', () => {
let api;
beforeEach(() => api = new RestClient(host));
it('should accept resource name and return resource', () => {
const cookies = api.res('cookies');
cookies.should.be.a('function');
});
it('should accept array of resource names and return array of resources', () => {
const t = api.res(['bees', 'cows']);
t.should.be.an('array');
});
it('should accept object of resource names and return object of resources', () => {
const t = api.res({
'bees': [
'big',
'small'
],
'cows': {
'white': 'good'
},
'dogs': 0
});
t.should.be.an('object');
api.bees.should.be.a('function');
api.bees.big.should.be.a('function');
api.bees.small.should.be.a('function');
api.cows.should.be.a('function');
api.cows.white.should.be.a('function');
api.cows.white.good.should.be.a('function');
api.dogs.should.be.a('function');
});
it('should make a shortcut for resource by default', () => {
api.should.not.have.property('cookies');
const cookies = api.res('cookies');
api.cookies.should.be.equal(cookies);
});
it('should make a shortcut for resource array by default', () => {
api.should.not.have.property('cookies');
api.should.not.have.property('cows');
const arr = api.res(['cookies', 'cows']);
api.cookies.should.be.equal(arr[0]);
api.cows.should.be.equal(arr[1]);
});
it('should not make a shortcut if pass option to constructor', () => {
const api = new RestClient(host, {shortcut: false});
api.should.not.have.property('cookies');
const cookies = api.res('cookies');
api.should.not.have.property('cookies');
});
it('should not make a shortcut if pass false to second option', () => {
api.should.not.have.property('cookies');
const cookies = api.res('cookies', false);
api.should.not.have.property('cookies');
});
it('should cache created resources', () => {
const cookies = api.res('cookies');
cookies.should.be.a('function');
const cookies2 = api.res('cookies');
cookies.should.be.eql(cookies2);
});
it('should add additional shortcuts for custom rules', () => {
const r = /(-)(.)/g;
const api = new RestClient(host, {
shortcutRules: [
resName => resName.replace(r, (match, p1, p2) => p2.toUpperCase()),
]
});
api.should.not.have.property('cookies-and-biscuits');
api.should.not.have.property('cookiesAndBiscuits');
const cookiesAndBiscuits = api.res('cookies-and-biscuits');
cookiesAndBiscuits.should.be.a('function');
api['cookies-and-biscuits'].should.be.equal(cookiesAndBiscuits);
api.cookiesAndBiscuits.should.be.equal(cookiesAndBiscuits);
});
});
describe('#url()', () => {
let api;
beforeEach(() => {
api = new RestClient(host);
api.res('cookies');
});
it('should build correct resource url', () => {
api.cookies.url().should.be.equal('/cookies');
});
it('should build correct resource instance url', () => {
api.cookies(42).url().should.be.equal('/cookies/42');
});
it('should build correct resource url if two in stack', () => {
api.cookies.res('bakers');
api.cookies(42).bakers(24).url().should.be.equal('/cookies/42/bakers/24');
});
it('should build correct resource url if more than two in stack', () => {
api.cookies.res('bakers').res('cats');
api.cookies(42).bakers.cats.url().should.be.equal('/cookies/42/bakers/cats');
api.cookies(42).bakers(24).cats(15).url().should.be.equal('/cookies/42/bakers/24/cats/15');
});
});
describe('#get()', () => {
let api;
beforeEach(() => {
api = new RestClient(host);
api.res('cookies');
});
it('should correct form query args when get one instance', () => {
let req;
xhr.onCreate = r => req = r;
api.cookies(4).get();
req.url.should.be.equal(host + '/cookies/4');
});
it('should correct form query args when get multiply instances', () => {
let req;
xhr.onCreate = r => req = r;
api.cookies.get({fresh: true});
req.url.should.be.equal(host + '/cookies?fresh=true');
});
it('should correct form query args when get multiply args', () => {
let req;
xhr.onCreate = r => req = r;
api.cookies.get({'filter[]': 'fresh'}, {'filter[]': 'taste'});
req.url.should.be.equal(host + '/cookies?filter%5B%5D=fresh&filter%5B%5D=taste');
});
it('should work correctly with an undefined content type', (done) => {
let req;
xhr.onCreate = r => req = r;
const p = api.cookies.get({fresh: true});
req.respond(200, [], '{a:1}');
req.url.should.be.equal(host + '/cookies?fresh=true');
p.then(r => {
r.should.be.equal('{a:1}');
done();
}).catch(done);
});
it('should correctly parse response', (done) => {
let req;
xhr.onCreate = r => req = r;
const p = api.cookies.get({fresh: true});
req.respond(200, {'Content-Type': 'application/json'}, '{"a":"1df"}');
req.url.should.be.equal(host + '/cookies?fresh=true');
p.then(r => {
r.should.be.deep.equal({"a": "1df"});
done();
}).catch(done);
});
it('should correctly handle exception with wrong encoded response body', (done) => {
let req;
xhr.onCreate = r => req = r;
sinon.spy(console, 'error');
sinon.spy(console, 'log');
const p = api.cookies.get({fresh: true});
req.respond(200, {'Content-Type': 'application/json'}, '{"a":1df}');
req.url.should.be.equal(host + '/cookies?fresh=true');
p.then(r => {
r.should.be.equal('{"a":1df}');
console.error.callCount.should.equal(1);
console.log.callCount.should.equal(3);
console.error.restore();
console.log.restore();
done();
}).catch(done);
});
it('should emit once event', (done) => {
let req;
xhr.onCreate = r => req = r;
let respText;
const p = api.cookies.get({fresh: true}).on('success', xhr => respText = xhr.responseText);
setTimeout(() => req.respond(200, [], '{a:1}'), 0);
req.url.should.be.equal(host + '/cookies?fresh=true');
p.then(r => {
r.should.be.equal('{a:1}');
respText.should.be.equal('{a:1}');
done();
}).catch(done);
});
it('should emit once request event', (done) => {
let req;
xhr.onCreate = r => req = r;
let bool;
const p = api.cookies.get({fresh: true}).on('request', xhr => bool = true);
setTimeout(() => req.respond(200, [], '{a:1}'), 0);
req.url.should.be.equal(host + '/cookies?fresh=true');
p.then(r => {
bool.should.be.equal(true);
done();
}).catch(done);
});
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2015",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"ES2021",
"DOM"
],
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "dist"
},
"include": ["./src/*"]
}
gitextract_hpts000u/ ├── .gitignore ├── LICENSE ├── README.adoc ├── build.js ├── examples/ │ ├── github.html │ └── github.ts ├── package.json ├── src/ │ ├── minivents.ts │ └── rest-client.ts ├── test/ │ └── test.js └── tsconfig.json
SYMBOL INDEX (21 symbols across 2 files)
FILE: src/minivents.ts
type Listener (line 1) | type Listener = (...p: any[]) => void
type Target (line 3) | type Target = {
function Events (line 9) | function Events<T>(target: T): T & Target {
FILE: src/rest-client.ts
function entries (line 3) | function entries<T extends Record<string, any>>(obj: T): ([Extract<keyof...
type Arg (line 7) | type Arg = Record<string, string | boolean | number>
function encodeUrl (line 9) | function encodeUrl(data: Arg) {
function safe (line 16) | function safe(func: Function, data: any) {
type CustomShortcut (line 29) | type CustomShortcut = (resName: string) => string
type Encodings (line 31) | type Encodings = {
type Opts (line 38) | type Opts = {
type Ress (line 46) | type Ress = string | readonly string[] | { [resName: string]: Ress | 0 |...
type MakeRes (line 48) | type MakeRes<T extends Ress> =
type MR (line 53) | type MR<T extends Ress> =
type Res (line 58) | interface Res {
method _client (line 66) | private get _client() {
method constructor (line 70) | constructor(private _c: () => RestClient, private _parent: Res | undef...
class Res (line 62) | class Res extends Function {
method _client (line 66) | private get _client() {
method constructor (line 70) | constructor(private _c: () => RestClient, private _parent: Res | undef...
class RestClient (line 176) | class RestClient extends Res implements Target {
method constructor (line 194) | constructor(host: string, options?: Partial<Opts> & Encodings) {
method conf (line 202) | conf(options: Partial<Opts> = {}): Opts {
method _request (line 224) | _request(method: string, url: string, data: any = null, contentType: s...
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (40K chars).
[
{
"path": ".gitignore",
"chars": 69,
"preview": "bower_components\nnode_modules\n.idea\nnpm-debug.log\ndist\nyarn-error.log"
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Amareis\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.adoc",
"chars": 12848,
"preview": "= another-rest-client\n\nSimple REST API client that makes your code lesser and more beautiful than without it.\n\nThere is "
},
{
"path": "build.js",
"chars": 1161,
"preview": "const path = require(\"path\");\nconst esbuild = require(\"esbuild\");\n\nlet outdir = path.resolve(process.cwd(), \"dist\");\n\nco"
},
{
"path": "examples/github.html",
"chars": 732,
"preview": "<html lang=\"en\">\n<head>\n <title>another-rest-client</title>\n <script src=\"../dist/rest-client.js\"></script>\n <s"
},
{
"path": "examples/github.ts",
"chars": 441,
"preview": "import RestClient from '../src/rest-client'\n\nconst api = new RestClient('https://api.github.com').withRes({\n repos: '"
},
{
"path": "package.json",
"chars": 848,
"preview": "{\n \"name\": \"another-rest-client\",\n \"version\": \"0.7.0\",\n \"description\": \"Simple REST API client that makes your code l"
},
{
"path": "src/minivents.ts",
"chars": 1174,
"preview": "export type Listener = (...p: any[]) => void\n\nexport type Target = {\n on: (type: string, func: Listener, ctx: any) =>"
},
{
"path": "src/rest-client.ts",
"chars": 8973,
"preview": "import {Events, Target} from './minivents.js'\n\nfunction entries<T extends Record<string, any>>(obj: T): ([Extract<keyof "
},
{
"path": "test/test.js",
"chars": 10010,
"preview": "require('chai').should();\nconst sinon = require('sinon');\nconst FormData = require('form-data');\n\nconst {RestClient} = r"
},
{
"path": "tsconfig.json",
"chars": 328,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2015\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": tr"
}
]
About this extraction
This page contains the full source code of the Amareis/another-rest-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (36.8 KB), approximately 9.2k tokens, and a symbol index with 21 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.