How do I use functional chaining with composition in javascript?

286 views Asked by At

I have this code that is building a url endpoint taking in different parameters:

const query = (obj) => {
  let starter = 'query='
  let queryToString = JSON.stringify(obj)
  return `${starter}${queryToString}`
}

const params = (str) => `${str}`

const endpoint = (protocol, base, params, query) => {
  if (!params && !query) return `${base}`
  if (!params) return `${base}${query}`
  return `${protocol}${base}?${params}&${query}`
}

const baseUrl = 'api.content.io'

const protocol = (secure = true) => secure ? 'https://' : 'http://'

let result = endpoint(protocol(), baseUrl, params('limit=5&order=desc'),
  query({
    field: 'title',
    include: 'brands'
  }));

console.log(result)

This builds a string like:

https: //api.content.io?limit=5&order=desc&query={"field":"title","include":"brands"}

Is it possible to refactor this code such that the conditionals inside the endpoint function can be removed, the right concatenation strings be applied and the whole thing chained into a functional call like

Endpoint.chain(protocol(p)).chain(base(b)).chain(params(p)).chain(query(q)).build()

How do I get started with doing this?

UPDATE: The solutions below are pretty good but I would like to understand how functional programmers use ADTs (Algebraic Data Types) with Monads to solve this problem. The idea is to run a chain of functions and then a fold to get back the value I want

3

There are 3 answers

0
Santanu Biswas On

Although the above answers will work, but if you want the syntax to start with Endpoint(). then use a class EndpointBuilder and a factory function like below.

class EndpointBuilder {

    constructor() {
        this.params = [];
        this.protocol = 'http://';
        this.baseUrl = 'api.endpoint.io';
    }

    base(url) {
        if (url && url.trim().length > 0)
            this.baseUrl = url;
        return this;
    }

    secure() {
        this.protocol = 'https://';
        return this;
    }

    setParam(param, val) {
        if (param && val)
            this.params.push({ param, val });
        return this
    }

    toString() {

        const url = `${this.protocol}${this.baseUrl}`;
        if (this.params.length <= 0)
            return url;

        let qString = '';
        this.params.forEach(p => {
            qString = `${qString}${qString.length > 0 ? '&' : ''}${p.param}=${JSON.stringify(p.val)}`;
        });

        return `${url}?${qString}`;
    }
};

// Endpoint Factory
const Endpoint = function () {
    return new EndpointBuilder();
};

Usage Example:

const url1 = Endpoint().setParam('limit', 5).setParam('query', { field: 'title', include: 'brands' }).toString();

const url2 = Endpoint().base('another.endpoint.io').setParam('limit', 5).setParam('order', 'desc').setParam('query', { field: 'title', include: 'brands' }).toString();

const url3 = Endpoint().base('another.endpoint.io').secure().setParam('limit', 5).setParam('order', 'desc').setParam('query', { field: 'title', include: 'brands' }).toString();

console.log(url1);   
// http://api.endpoint.io?limit=5&query={"field":"title","include":"brands"}

console.log(url2);
// http://another.endpoint.io?limit=5&order="desc"&query={"field":"title","include":"brands"}

console.log(url3);
// https://another.endpoint.io?limit=5&order="desc"&query={"field":"title","include":"brands"}
3
Shanimal On

Are you looking for something like this?

Chaining basically happens when you return the complete object in every function call. Here each function sets the value of its given chunk and returns the this object for whatever context it's attached to. Each function would be able to process the value it receives and the value function would build the url based on the set parameters.

var Endpoint = {
    protocol: protocol,
    base: base,
    params: params,
    query: query,
    value: value
}
function protocol(b) {
    protocol.value = b && 'https://' || 'http://';
    return this;
}
function base(s) {
    base.value = s || '';
    return this;
}
function params(s) {
    params.value = s || '';
    return this;
}
function query(s) {
    query.value = s || '';
    return this;
}
function value(s) {
    return   '$protocol$base$params$params$query'
        .replace('$protocol',protocol.value)
        .replace('$base',base.value)
        .replace('$params',params.value)
        .replace('$query',query.value)
}

Endpoint
    .protocol(true)
    .base('www.foo.com')
    .params('one/two/three')
    .query('?foo=bar')
    .value()
// "https://www.foo.comone/two/three$params?foo=bar"
0
trincot On

You could use a class for that, which has methods that allow chaining (they return this):

class EndPoint {
    constructor() {
        // defaults
        this.prot = 'https://';
        this.bas = 'api.content.io';
        this.qry = '';
        this.par = '';
    }
    secure(on = true) {
        this.prot = on ? 'https://' : 'http://';
        return this;
    }
    base(str) {
        this.bas = str;
        return this;
    }
    query(obj) {
        this.qry = `query=${JSON.stringify(obj)}`
        return this;
    }
    params(str) {
        this.par = str
        return this;
    }
    build() {
        let sep1 = this.qry.length || this.par.length ? '?' : '';
        let sep2 = this.qry.length && this.par.length ? '&' : '';
        return `${this.prot}${this.bas}${sep1}${this.par}${sep2}${this.qry}`;
    }
};

let result = new EndPoint().secure(true).base('www.example.com')
    .params('limit=5&order=desc').query({ field: 'title', include: 'brands' }).build();

console.log(result);