Coder Social home page Coder Social logo

tillathehun0 / murry Goto Github PK

View Code? Open in Web Editor NEW
4.0 4.0 1.0 1.47 MB

Separate controller and router handling with currying

License: MIT License

JavaScript 1.96% TypeScript 98.04%
rest-api express hapi restify currying marshalling ramda controllers curry functional-programming

murry's Introduction

murry

Coverage Status Maintainability Build Status JavaScript Style Guide npm version License Greenkeeper badge

murry keeps your controller separate from your routing logic via curried marshalling. This keeps your controller logic separate from whatever framework you choose to handle web-server routing while currying makes it highly composable and prevents duplicated code

Table of Contents

Installation

npm

$ npm install --save murry

yarn

$ yarn add murry

Currying?

Currying is a technique used in functional programming for buiilding composable reusuable functions. Instead of repeating it, read a good post about currying here

Marshalling?

From WikiPedia:

In computer science, marshalling or marshaling is the process of transforming the memory representation of an object to a data format suitable for storage or transmission,and it is typically used when data must be moved between different parts of a computer program or from one program to another...It simplifies complex communication, using composite objects in order to communicate...

The term Mashalling is most commonly used when when talking about objects in memory or serialization for sending objects across a network. Murry technically isn't marshalling, but from a high level sense it is; it is taking the shape of some objects, which the programmer cannot control and is external to his or her code, and turns it into a state that our code can work with.

This separates any web-server library specifics from your business logic. This means controllers are much easier to test, as they do not require mocking objects created by an external web-server library. It also promotes separation of concerns, as code for extracting data from incoming requests, and responding to those requests is separate from the "meat" of your program.

How It Works

At the root of Murry is a curried function like so:

function murryer (errorHandler, extractor, responder, controller) {
  return (...args) { // args given to murry by the web-server library

    // extract the data we want from args
    // pass that data to our controller, which just returns a promise
    // respond to the request using the data provided by the controller

    // catch errors, also handling them as the web-server library would

  }
}

Lets go through each argument

  • errorHandler Function<Function (...args)>(err: Error): a function that itself returns a function that handles any errors that arise while handling a request
  • extractor Function(...args): a function that extracts the data we want from args and returns them as an Array. These values are spread into the controller function
  • responder Function<Function<>>(...args): a function that itself returns a function that handles responding to the request using the data returned from the controller call
  • controller Function(...args): a function that returns a promise. What is resolved from the promise returned from the controller will be passed to the responder

args are the arguments provided by whatever web-server library you are using. Since the function is curried, we can pass any number of these arguments and then use those new functions to compose distinct flows. This composability compounds for each argument! Utilizing the currying capabilities, you can set up defaults for all or a subset of your murryers.

Murry has a tiny footprint; It's just a couple functions and some sensisble prebuilt default extractors, responders, and errorHandlers for ExpressJS

Usage

import { curryer } from 'murry'

/**
 * With Express JS
 * 
 * Just call next() and pass it an error argument, which tells Express that an error has occurred
 * and immediatley skips all middleware until the error handling middleware
 **/
const errorHandler = err =>
  (req, res, next) => next(err)

// We only pass in our error handler to the curryer
const defaultMurryer = curryer(errorHandler)

/**
 * Set up lots of murryers with different extractors, but with the same error handling!
 * 
 * Extractors always need to return an array.
 **/
const getReqMurryer = defaultMurryer((req, res, next) => [req.params, req.query]) // extract the url params and query string
const postReqMurryer = defaultMurryer((req, res, next) => [req.body]) // extract the body
const deleteReqMurryer = defaultMurryer((req, res, next) => [req.params]) // extract just the url params

// Now use those murryers for even more flow!
const postReqJsonResMurryer = postReqMurryer(data => (req, res, next) =>  res.json(data))
const postReqStatusResMurryer= postReqMurryer(() => (req, res, next) => res.sendStatus(203))
// ... so on

// =========================================

// Then on a endpoint route somewhere...
import { postReqJsonResMurryer } from '../my.murryers'

/**
 * a controller function that is given the spread values provided by the extractor
 * 
 * It just returns a promise
 */
const controller = async body => {
  body.type = 'foo'
  // whatever else we do with it
  // post the new asset to the database!
  return { id: 1, ...body } // return mocked new persisted asset
}

/**
 * Set up the route on the router
 * 
 * The Murryer extracts the body, invokes the controller
 * providing the extract values, and responds to the client with JSON
 * from the controller
 */
router.post('/api/assets', someOtherMiddleware(), postReqJsonResMurryer(controller))

In the example above, notice that the controller function has no specific logic for handling request objects, or responding to the client, or handling errors. You've already set up all of that route handling in your Murryer! The controller is just passed the extracted values as arguements and then it just returns a Promise. This controller is now encapsulated and kept separate from the web-server library. It is much easier to unit test and does not require mocking objects for that whatever web-server library you are using.

Decide you want to use a different web-server library, say Restify, instead of ExpressJS? No problem! Just adjust the Murryer to extract and repsond w.r.t the new library and your controllers and tests just work!

Decide you want different error handling? Just update the one function and boom. It's updated for all routes using that Murryer.

Need very specific handling for a route? Just compose a murryer from scratch without touching your other murryers.

The high composability of murry allows you to easily write reusable implementation, minimize changes during refactors, and keep your logic separate from the web-server library

With Ramda

Ramda is a great library for embracing functional programming. For example, all Ramda functions are automatically curried. One great tool Ramda provides is the __ function. __ can be used as a placeholder for functions being passed to a curry function. Say you needed a murryer that used an extractor and responder, but needed to be composable to have different error handling? No problem:

import { curryer } from 'murry'
import { __ } from 'ramda'

const extractor = req => [req.body]
const responder = (req, res) => data => res.json(data)

const defaultMurryer = curryer(__, extractor, responder) // placehold the errorHandler

Then you can pass in the errorHandler later. This also means arguments passed to the curried function can be provided out of order!

TODO

  • Plugin system for web-server libraries to make murry work out of the box with less boilerplate
  • Better docs
  • Library specific tests ie. Express, Hapi, Restify, etc.

Contribute

Submit an issue or a PR

License

MIT

Name

"marshall + curry" => "murry". Murryer also rhymes with Courier which sort of aligns with the use case for murry :)

murry's People

Contributors

dependabot[bot] avatar greenkeeper[bot] avatar richard-peterson-at-lifelogic avatar tillathehun0 avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

murry's Issues

An in-range update of commitlint is breaking the build 🚨

There have been updates to the commitlint monorepo:

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

This monorepo update includes releases of one or more dependencies which all belong to the commitlint group definition.

commitlint is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build could not complete due to an error (Details).

Commits

The new version differs by 39 commits.

  • c17420d v8.1.0
  • ca19d70 chore: update dependency lodash to v4.17.14 (#724)
  • 5757ef2 build(deps): bump lodash.template from 4.4.0 to 4.5.0 (#721)
  • 5b5f855 build(deps): bump lodash.merge from 4.6.0 to 4.6.2 (#722)
  • 4cb979d build(deps): bump lodash from 4.17.11 to 4.17.13 (#723)
  • a89c1ba chore: add devcontainer setup
  • 9aa5709 chore: pin dependencies (#714)
  • c9ef5e2 chore: centralize typescript and jest setups (#710)
  • c9dcf1a chore: pin dependencies (#708)
  • 6a6a8b0 refactor: rewrite top level to typescript (#679)
  • 0fedbc0 chore: update dependency @types/jest to v24.0.15 (#694)
  • 0b9c7ed chore: update dependency typescript to v3.5.2 (#695)
  • 4efb34b chore: update dependency globby to v10 (#705)
  • 804af8b chore: update dependency lint-staged to v8.2.1 (#696)
  • 9075844 fix: add explicit dependency on chalk (#687)

There are 39 commits in total.

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of @types/ramda is breaking the build 🚨

The devDependency @types/ramda was updated from 0.26.36 to 0.26.37.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/ramda is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build could not complete due to an error (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.