Skip to content

Commit 622d479

Browse files
committed
Initial commit
0 parents  commit 622d479

25 files changed

+3616
-0
lines changed

Diff for: .babelrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"plugins": [
3+
"transform-runtime",
4+
"transform-flow-strip-types"
5+
],
6+
"presets": [
7+
"es2015",
8+
"stage-0"
9+
]
10+
}

Diff for: .editorconfig

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# EditorConfig helps developers define and maintain consistent
2+
# coding styles between different editors and IDEs
3+
# editorconfig.org
4+
5+
root = true
6+
7+
[*]
8+
9+
# Change these settings to your own preference
10+
indent_style = space
11+
indent_size = 2
12+
13+
# We recommend you to keep these unchanged
14+
end_of_line = lf
15+
charset = utf-8
16+
trim_trailing_whitespace = true
17+
insert_final_newline = true
18+
19+
[*.md]
20+
trim_trailing_whitespace = false

Diff for: .eslintrc.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
'env': {
3+
'es6': true,
4+
'jest': true,
5+
'node': true,
6+
},
7+
'parser': 'babel-eslint',
8+
'parserOptions': {
9+
ecmaVersion: 7,
10+
sourceType: 'module'
11+
},
12+
'plugins': ['import'],
13+
'extends': 'eslint:recommended',
14+
'rules':{
15+
'no-console': 0,
16+
}
17+
};

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/lib/
2+
/node_modules/

Diff for: .travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: node_js
2+
3+
node_js:
4+
- "7"
5+
- "6"
6+
7+
cache:
8+
directories:
9+
- node_modules
10+
11+
script:
12+
- npm run lint
13+
- npm run build

Diff for: LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Kévin Dunglas
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Diff for: README.md

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# API Platform CRUD Generator
2+
3+
A generator to scaffold a React/Redux app with Create-Retrieve-Update-Delete features for any API exposing a Hydra documentation.
4+
Works especially well with APIs built with the [API Platform](https://api-platform.com) framework.
5+
6+
## Features
7+
8+
* Generate a working ES6 application built with [React](https://facebook.github.io/react/), [Redux](http://redux.js.org), [React Router](https://reacttraining.com/react-router/) and [Redux Form](http://redux-form.com/)
9+
* List
10+
* Create form with appropriate form inputs depending of the documented type and client-side validation (required fields)
11+
* Update form
12+
* Errors handling
13+
* Deletion
14+
* [Bootstrap](https://getbootstrap.com/) support
15+
16+
## Installation and Usage
17+
18+
Create a React application using Facebook's Create React App:
19+
20+
$ create-react-app my-app
21+
$ cd my-app
22+
23+
Install React Router, Redux, React Redux, React Router Redux, Redux Form and Redux Thunk (to handle AJAX requests):
24+
25+
$ yarn add redux react-redux redux-thunk redux-form react-router-dom react-router-redux
26+
27+
Install the generator globally:
28+
29+
$ yarn global add api-platform-generate-crud
30+
31+
Reference the Bootstrap CSS stylesheet in `public/index.html` (optional):
32+
33+
```html
34+
<!-- ... -->
35+
<title>React App</title>
36+
37+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
38+
</head>
39+
<!-- ... -->
40+
```
41+
42+
In the app directory, generate the files for the resource you want:
43+
44+
$ api-platform-generate-crud http://localhost src/ --resource foo
45+
# Replace the URL by the entrypoint of your Hydra-enabled API
46+
# Omit the resource flag to generate files for all resource types exposed by the API
47+
48+
The code is ready to be executed! Register the generated reducers and components in the `index.js` file, here is an example:
49+
50+
```javascript
51+
import React from 'react';
52+
import ReactDom from 'react-dom';
53+
import { createStore, combineReducers, applyMiddleware } from 'redux';
54+
import { Provider } from 'react-redux';
55+
import thunk from 'redux-thunk';
56+
import { reducer as form } from 'redux-form';
57+
import { BrowserRouter as Router, Route } from 'react-router-dom';
58+
import createBrowserHistory from 'history/createBrowserHistory';
59+
import { syncHistoryWithStore, routerReducer as routing } from 'react-router-redux'
60+
61+
// Replace "foo" by the name of your resource
62+
import foo from './reducers/foo/';
63+
import FooList from './components/foo/List';
64+
import FooCreate from './components/foo/Create';
65+
import FooUpdate from './components/foo/Update';
66+
67+
const store = createStore(
68+
combineReducers({routing, form, foo}),
69+
applyMiddleware(thunk),
70+
);
71+
72+
const history = syncHistoryWithStore(createBrowserHistory(), store);
73+
74+
ReactDom.render(
75+
<Provider store={store}>
76+
<Router history={history}>
77+
<div>
78+
{/*Replace URLs and components accordingly*/}
79+
<Route exact={true} path='/foos/' component={FooList}/>
80+
<Route exact={true} path='/foos/create' component={FooCreate}/>
81+
<Route exact={true} path='/foos/edit/:id' component={FooUpdate}/>
82+
</div>
83+
</Router>
84+
</Provider>,
85+
document.getElementById('root')
86+
);
87+
```
88+
89+
## TODO
90+
91+
* Add support for pagination
92+
* Automatically normalize numbers
93+
* Support the (proprietary) API Platform mechanism for field errors
94+
* Generate E2E tests
95+
* Add a React Native generator
96+
* Add support for relations?
97+
98+
## Run tests
99+
100+
$ yarn test
101+
$ yarn lint
102+
103+
## Credits
104+
105+
Created by [Kévin Dunglas](https://dunglas.fr). Sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop).

Diff for: package.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "api-platform-generate-crud",
3+
"version": "0.1.0",
4+
"description": "Generate a CRUD application built with React, Redux and React Router from an Hydra-enabled API",
5+
"files": [
6+
"*.md",
7+
"docs/*.md",
8+
"lib",
9+
"src",
10+
"templates"
11+
],
12+
"main": "lib/index",
13+
"repository": "api-platform/generate-crud",
14+
"homepage": "https://github.com/api-platform/generate-crud",
15+
"bugs": "https://github.com/api-platform/generate-crud/issues",
16+
"author": "Kévin Dunglas",
17+
"license": "MIT",
18+
"devDependencies": {
19+
"babel-cli": "^6.24.0",
20+
"babel-core": "^6.24.0",
21+
"babel-eslint": "^7.2.0",
22+
"babel-plugin-transform-flow-strip-types": "^6.22.0",
23+
"babel-plugin-transform-runtime": "^6.23.0",
24+
"babel-preset-es2015": "^6.24.0",
25+
"babel-preset-stage-0": "^6.22.0",
26+
"eslint": "^3.18.0",
27+
"eslint-plugin-import": "^2.2.0"
28+
},
29+
"dependencies": {
30+
"api-doc-parser": "^0.1.1",
31+
"babel-runtime": "^6.23.0",
32+
"commander": "^2.9.0",
33+
"handlebars": "^4.0.6",
34+
"isomorphic-fetch": "^2.2.1",
35+
"mkdirp": "^0.5.1"
36+
},
37+
"scripts": {
38+
"lint": "eslint src",
39+
"build": "babel src -d lib --ignore '*.test.js'",
40+
"watch": "babel --watch src -d lib --ignore '*.test.js'"
41+
},
42+
"bin": {
43+
"api-platform-generate-crud": "./lib/index.js"
44+
}
45+
}

Diff for: src/ReactCrudGenerator.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import mkdirp from 'mkdirp';
2+
import handlebars from 'handlebars';
3+
import fs from 'fs';
4+
5+
export default class ReactCrudGenerator {
6+
templates = {};
7+
8+
constructor(hydraPrefix) {
9+
const templatePath = `${__dirname}/../templates/react/`;
10+
11+
this.hydraPrefix = hydraPrefix;
12+
13+
// actions
14+
this.registerTemplate(templatePath, 'actions/foo/create.js');
15+
this.registerTemplate(templatePath, 'actions/foo/delete.js');
16+
this.registerTemplate(templatePath, 'actions/foo/list.js');
17+
this.registerTemplate(templatePath, 'actions/foo/update.js');
18+
19+
// api
20+
this.registerTemplate(templatePath, 'api/fooFetch.js');
21+
22+
// components
23+
this.registerTemplate(templatePath, 'components/foo/Create.js');
24+
this.registerTemplate(templatePath, 'components/foo/Form.js');
25+
this.registerTemplate(templatePath, 'components/foo/List.js');
26+
this.registerTemplate(templatePath, 'components/foo/Update.js');
27+
28+
// reducers
29+
this.registerTemplate(templatePath, 'reducers/foo/create.js');
30+
this.registerTemplate(templatePath, 'reducers//foo/delete.js');
31+
this.registerTemplate(templatePath, 'reducers/foo/index.js');
32+
this.registerTemplate(templatePath, 'reducers/foo/list.js');
33+
this.registerTemplate(templatePath, 'reducers/foo/update.js');
34+
}
35+
36+
registerTemplate(templatePath, path) {
37+
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath+path).toString());
38+
}
39+
40+
generate(api, resource, dir) {
41+
const lc = resource.title.toLowerCase();
42+
43+
const context = {
44+
title: resource.title,
45+
name: resource.name,
46+
lc,
47+
uc: resource.title.toUpperCase(),
48+
fields: resource.readableFields,
49+
formFields: this.buildFields(resource.writableFields),
50+
hydraPrefix: this.hydraPrefix,
51+
};
52+
53+
// Create directories
54+
mkdirp.sync(`${dir}/api`); // This directory may already exist
55+
this.createDir(`${dir}/actions/${lc}`);
56+
this.createDir(`${dir}/components/${lc}`);
57+
this.createDir(`${dir}/reducers/${lc}`);
58+
59+
// actions
60+
this.createFile('actions/foo/create.js', `${dir}/actions/${lc}/create.js`, context);
61+
this.createFile('actions/foo/delete.js', `${dir}/actions/${lc}/delete.js`, context);
62+
this.createFile('actions/foo/list.js', `${dir}/actions/${lc}/list.js`, context);
63+
this.createFile('actions/foo/update.js', `${dir}/actions/${lc}/update.js`, context);
64+
65+
// api
66+
this.createFile('api/fooFetch.js', `${dir}/api/${lc}Fetch.js`, context);
67+
68+
// components
69+
this.createFile('components/foo/Create.js', `${dir}/components/${lc}/Create.js`, context);
70+
this.createFile('components/foo/Form.js', `${dir}/components/${lc}/Form.js`, context);
71+
this.createFile('components/foo/List.js', `${dir}/components/${lc}/List.js`, context);
72+
this.createFile('components/foo/Update.js', `${dir}/components/${lc}/Update.js`, context);
73+
74+
// reducers
75+
this.createFile('reducers/foo/create.js', `${dir}/reducers/${lc}/create.js`, context);
76+
this.createFile('reducers//foo/delete.js', `${dir}/reducers/${lc}/delete.js`, context);
77+
this.createFile('reducers/foo/index.js', `${dir}/reducers/${lc}/index.js`, context);
78+
this.createFile('reducers/foo/list.js', `${dir}/reducers/${lc}/list.js`, context);
79+
this.createFile('reducers/foo/update.js', `${dir}/reducers/${lc}/update.js`, context);
80+
}
81+
82+
getInputTypeFromField(field) {
83+
switch (field.id) {
84+
case 'http://schema.org/email':
85+
return {type: 'email'};
86+
87+
case 'http://schema.org/url':
88+
return {type: 'url'};
89+
}
90+
91+
switch (field.range) {
92+
case 'http://www.w3.org/2001/XMLSchema#integer':
93+
return {type: 'number'};
94+
95+
case 'http://www.w3.org/2001/XMLSchema#decimal':
96+
case 'http://www.w3.org/2001/XMLSchema#number':
97+
return {type: 'number', step: '0.1'};
98+
99+
case 'http://www.w3.org/2001/XMLSchema#boolean':
100+
return {type: 'checkbox'};
101+
102+
case 'http://www.w3.org/2001/XMLSchema#date':
103+
return {type: 'date'};
104+
105+
case 'http://www.w3.org/2001/XMLSchema#time':
106+
return {type: 'time'};
107+
108+
case 'http://www.w3.org/2001/XMLSchema#dateTime':
109+
return {type: 'datetime'};
110+
111+
default:
112+
return {type: 'text'};
113+
}
114+
}
115+
116+
buildFields(apiFields) {
117+
let fields = [];
118+
for (let apiField of apiFields) {
119+
if (null !== apiField.reference) {
120+
// References are ignored for now
121+
continue;
122+
}
123+
124+
let field = this.getInputTypeFromField(apiField);
125+
field.required = apiField.required;
126+
field.name = apiField.name;
127+
field.description = apiField.description;
128+
129+
fields.push(field)
130+
}
131+
132+
return fields;
133+
}
134+
135+
createDir(dir) {
136+
if (fs.existsSync(dir)) throw new Error(`The directory "${dir}" already exists`);
137+
mkdirp.sync(dir);
138+
}
139+
140+
createFile(template, dest, context) {
141+
fs.writeFileSync(dest, this.templates[template](context));
142+
}
143+
}

0 commit comments

Comments
 (0)