Skip to content

Commit

Permalink
feat(http): refactored, documented and added factories
Browse files Browse the repository at this point in the history
  • Loading branch information
christianalfoni committed Nov 18, 2016
1 parent 4bb8329 commit 0fb578f
Show file tree
Hide file tree
Showing 12 changed files with 719 additions and 175 deletions.
295 changes: 243 additions & 52 deletions packages/cerebral-provider-http/README.md
Expand Up @@ -3,80 +3,271 @@ HTTP Provider for Cerebral2
Based on prior art from [cerebral-module-http](https://github.com/cerebral/cerebral-module-http)

### Install
This is still alpha but to test the bleeding edge

`npm install cerebral-provider-http@next --save`

### Todos
Create npm install
Test FileUpload
### Instantiate

### How to use
```js
import { Controller } from 'cerebral'
import {Controller} from 'cerebral'
import HttpProvider from 'cerebral-provider-http'
import { FileUpload } from 'cerebral-provider-http'

const controller = Controller({
state: {
githubUser: {}
},
providers: [
HttpProvider({
baseUrl: 'https://api.github.com'
// Prefix all requests with this url
baseUrl: 'https://api.github.com',

// Any default headers to pass on requests
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json'
},

// When talking to cross origin (cors), pass cookies
// if set to true
withCredentials: false
})
],
signals: {
getUser: [getGithubUser, {
success: [setData],
error: [logError]
}, [setUrlEncode, getGithubUser, {
error: [checkError]
}]]
}
]
})
```

controller.getSignal("getUser")()
You can update these default options in an action:

function getGithubUser({state, path, http}) {
return http.get("/users/fopsdev")
.then(response => path.success({
result: response.result
}))
.catch(error => path.error({
error: error
}))
```js
function updateDefaultHttpOptions({http}) {
http.updateOptions({
// Updated options
})
}
```

### Request

#### Custom request

```js
function someGetAction ({http}) {
return http.request({
// Any http method
method: 'GET',

// Url you want to request to
url: '/items'

// Request body as object. Will automatically be stringified if json and
// urlEncoded if application/x-www-form-urlencoded
body: {},

// Query as object, will automatically be urlEncoded
query: {},

// If cross origin request, pass cookies
withCredentials: false,

// Any additional http headers, or overwrite default
headers: {},

function setData({input, state}) {
state.set('githubUser', input.result)
let res = state.get('githubUser')
if (res.login === 'fopsdev')
console.log('basic http get test succeeded!')
// A function or signal path (foo.bar.requestProgressed) that
// triggers on request progress. Passes {progress: 45} etc.
onProgress: null
})
}
```

#### Convenience methods

function logError({input}) {
console.warn('basic http get test failed :' + JSON.stringify(input.error))
```js
function someGetAction ({http}) {
return http.get('/items', {
// QUERY object
}, {
// Any options defined in "Custom request"
})
}

function setUrlEncode({http}) {
http.updateOptions({
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
function somePostAction ({http}) {
return http.post('/items', {
// BODY object
}, {
// Any options defined in "Custom request"
})
}

function somePutAction ({http}) {
return http.put('/items/1', {
// BODY object
}, {
// Any options defined in "Custom request"
})
}

// same get request but with content type urlencoded should end here
// not 100% safe i know but it is a test :)
function checkError({input}) {
console.log('basic http get with urlencoded test succeeded')
function somePatchAction ({http}) {
return http.patch('/items/1', {
// BODY object
}, {
// Any options defined in "Custom request"
})
}

function someDeleteAction ({http}) {
return http.delete('/items/1', {
// QUERY object
}, {
// Any options defined in "Custom request"
})
}
```

#### Factories
```js
import {httpGet, httpPost, httpPut, httpPatch, httpDelete} from 'cerebral-provider-http'
import {string, input, state} from 'cerebral/operators'

export default [
// Static
httpGet('/items'), {
success: [],
error: []
},
// Dynamic
httpGet(string`/items/${input`itemId`}`), {
success: [],
error: []
},
// Pass data, either query or body, depending on factory used
httpPost('/items', {
title: input`itemTitle`,
foo: 'bar'
}), {
success: [],
error: []
},
// You can handle aborted if you use that functionality
httpPut('/items'), {
success: [],
error: [],
abort: []
},
// They all have this functionality
httpPatch(string`/items/${input`itemId`}`, state`patchData`), {
success: [],
error: []
},
// Kinda cool? :)
httpDelete(string`/items/${state`currentItemId`}`), {
success: [],
error: []
}
]
```

### Response

```js
function someGetAction ({http}) {
return http.get('/items')
// All status codes between 200 - 300, including 200
.then((response) => {
response.status // Status code of response
response.result // Parsed response text
})
// All other status codes
.catch((response) => {
response.status // Status code of response
response.result // Parsed response text
})
}
```

### Abort request
You can abort any running request, causing the request to resolve as status code **0** and set an **isAborted** property on the response object.

```js
function searchItems({input, state, path, http}) {
http.abort('/items*') // regexp string
return http.get(`/items?query=${input.query}`)
.then(path.success)
.catch((response) => {
if (response.isAborted) {
return path.abort()
}

return path.error(response)
})
}

// todo FileUpload Testing
// var fileUpload = FileUpload({
// url: "http://posttestserver.com/post.php?dir=example"
// })
// console.log(fileUpload)
// fileUpload.send(wuuut?)
export default [
searchItems, {
success: [],
error: [],
abort: []
}
]
```

### Cors
Cors has been turned into a "black box" by jQuery. Cors is actually a very simple concept, but due to a lot of confusion of "Request not allowed", **cors** has been an option to help out. In HttpProvider we try to give you the insight to understand how cors actually works.

Cors has nothing to do with the client. The only client configuration related to cors is the **withCredentials** option, which makes sure cookies are passed to the cross origin server. The only requirement for cors to work is that you pass the correct **Content-Type**. Now, this depends on the server in question. Some servers allows any content-type, others require a specific one. These are the typical ones:

- text/plain
- application/x-www-form-urlencoded
- application/json; charset=UTF-8

Note that this is only related to the **request**. If you want to define what you want as response, you set the **Accept** header, which is *application/json* by default.

### File upload
Since Cerebral can only use serializable data in signals and the state tree, any file uploads must happen in components.

```js
import {FileUpload} from 'cerebral-module-http'

export default connect({
fileNames: 'app.fileNames'
}, {
filesAdded: 'app.filesAdded',
uploadStarted: 'app.uploadStarted',
uploadProgressed: 'app.uploadProgressed',
uploadFinished: 'app.uploadFinished',
uploadFailed: 'app.uploadFailed'
},
class UploadFile extends Component {
constructor (props) {
super(props);
this.filesToUpload = [];
}
onFilesChange (event) {
this.filesToUpload = event.target.files;
this.props.filesAdded({
fileNames: this.filesToUpload.map(file => file.name)
})
}
upload () {
const fileUpload = new FileUpload({
url: '/upload',
headers: {},
// Additional data on form. Do not use "file", it is taken
data: {},
// Triggers with object {progress: 54} and so on
onProgress: this.props.uploadProgressed
})

this.props.uploadStarted()
fileUpload.send(this.filesToUpload)
.then(this.props.uploadFinished)
.catch(this.props.uploadFailed)
}
render() {
return (
<h4>Please choose a file.</h4>
<div>
<input type="" onChange={(event) => this.onFilesChange(event)}/><br/><br/>
<button disabled={this.filesToUpload.length === 0} onClick={() => this.upload()}>Upload</button>
</div>
)
}
}
)
```

You are also able to abort file uploads by calling fileUpload.abort(). This will result in a rejection of the send promise. The data passed will be: **{status: 0, result: null, isAborted: true}**.
8 changes: 6 additions & 2 deletions packages/cerebral-provider-http/package.json
Expand Up @@ -4,6 +4,7 @@
"description": "HTTP provider for Cerebral 2",
"main": "lib/index.js",
"scripts": {
"test": "../../node_modules/.bin/mocha --compilers js:../../node_modules/babel-register 'src/**/*.test.js'",
"build": "BABEL_ENV=production ../../node_modules/.bin/babel src/ --out-dir=lib/ -s",
"prepublish": "npm run build"
},
Expand All @@ -20,9 +21,12 @@
"bugs": {
"url": "https://github.com/cerebral/cerebral/issues"
},
"homepage":
"https://github.com/cerebral/cerebral/tree/master/packages/cerebral-provider-http#readme",
"homepage": "https://github.com/cerebral/cerebral/tree/master/packages/cerebral-provider-http#readme",
"peerDependencies": {
"cerebral": "next"
},
"devDependencies": {
"cerebral": "next",
"xhr-mock": "https://github.com/christianalfoni/xhr-mock.git"
}
}
44 changes: 44 additions & 0 deletions packages/cerebral-provider-http/src/DEFAULT_OPTIONS.js
@@ -0,0 +1,44 @@
import {urlEncode} from './utils'

export default {
method: 'get',
baseUrl: '',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json'
},
onRequest (xhr, options) {
if (options.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
options.body = urlEncode(options.body)
} else if (options.headers['Content-Type'].indexOf('application/json') >= 0) {
options.body = JSON.stringify(options.body)
}

xhr.withCredentials = Boolean(options.withCredentials)

Object.keys(options.headers).forEach((key) => {
xhr.setRequestHeader(key, options.headers[key])
})

xhr.send(options.body)
},
onResponse (xhr, resolve, reject) {
let result = xhr.responseText

if (result && xhr.getResponseHeader('Content-Type').indexOf('application/json') >= 0) {
result = JSON.parse(xhr.responseText)
}

if (xhr.status >= 200 && xhr.status < 300) {
resolve({
status: xhr.status,
result: result
})
} else {
reject({
status: xhr.status,
result: result
})
}
}
}

0 comments on commit 0fb578f

Please sign in to comment.