Skip to content

WIP: scaffold skeleton for React Native #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"lint": "eslint src",
"build": "babel src -d lib --ignore '*.test.js'",
"watch": "babel --watch src -d lib --ignore '*.test.js'",
"test-gen": "rm -rf ./tmp && npm run build && ./lib/index.js https://demo.api-platform.com ./tmp",
"test-gen": "rm -rf ./tmp && npm run build && ./lib/index.js https://demo.api-platform.com ./tmp/react && ./lib/index.js https://demo.api-platform.com ./tmp/react-native -g react-native",
"test-gen-env": "rm -rf ./tmp && npm run build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js"
},
"bin": {
Expand Down
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ReactCrudGenerator from './generators/ReactCrudGenerator';
import ReactNativeCrudGenerator from './generators/ReactNativeCrudGenerator';
import TypescriptInterfaceGenerator from './generators/TypescriptInterfaceGenerator';

function wrap (cl) {
Expand All @@ -9,6 +10,8 @@ function generators (generator = 'react') {
switch (generator) {
case 'react':
return wrap(ReactCrudGenerator);
case 'react-native':
return wrap(ReactNativeCrudGenerator);
case 'typescript':
return wrap(TypescriptInterfaceGenerator);
}
Expand Down
29 changes: 15 additions & 14 deletions src/generators/ReactCrudGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ export default class ReactCrudGenerator {
templates = {};

constructor({hydraPrefix, templateDirectory}) {
const templatePathCommon = `${templateDirectory}/react-common/`;
const templatePath = `${templateDirectory}/react/`;

this.hydraPrefix = hydraPrefix;

// actions
this.registerTemplate(templatePath, 'actions/foo/create.js');
this.registerTemplate(templatePath, 'actions/foo/delete.js');
this.registerTemplate(templatePath, 'actions/foo/list.js');
this.registerTemplate(templatePath, 'actions/foo/update.js');
this.registerTemplate(templatePath, 'actions/foo/show.js');
this.registerTemplate(templatePathCommon, 'actions/foo/create.js');
this.registerTemplate(templatePathCommon, 'actions/foo/delete.js');
this.registerTemplate(templatePathCommon, 'actions/foo/list.js');
this.registerTemplate(templatePathCommon, 'actions/foo/update.js');
this.registerTemplate(templatePathCommon, 'actions/foo/show.js');

// api
this.registerTemplate(templatePath, 'api/fooFetch.js');
this.registerTemplate(templatePathCommon, 'api/fooFetch.js');

// components
this.registerTemplate(templatePath, 'components/foo/Create.js');
Expand All @@ -31,25 +32,25 @@ export default class ReactCrudGenerator {
this.registerTemplate(templatePath, 'components/foo/Show.js');

// reducers
this.registerTemplate(templatePath, 'reducers/foo/create.js');
this.registerTemplate(templatePath, 'reducers//foo/delete.js');
this.registerTemplate(templatePath, 'reducers/foo/index.js');
this.registerTemplate(templatePath, 'reducers/foo/list.js');
this.registerTemplate(templatePath, 'reducers/foo/update.js');
this.registerTemplate(templatePath, 'reducers/foo/show.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/create.js');
this.registerTemplate(templatePathCommon, 'reducers//foo/delete.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/index.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/list.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/update.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/show.js');

// routes
this.registerTemplate(templatePath, 'routes/foo.js');

// entrypoint
this.registerTemplate(templatePath, 'api/_entrypoint.js');
this.registerTemplate(templatePathCommon, 'api/_entrypoint.js');

// utils
this.registerTemplate(templatePath, 'utils/helpers.js');
}

registerTemplate(templatePath, path) {
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath+path).toString());
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath + path).toString());
}

help(resource) {
Expand Down
186 changes: 186 additions & 0 deletions src/generators/ReactNativeCrudGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import mkdirp from 'mkdirp';
import handlebars from 'handlebars';
import fs from 'fs';
import urlapi from 'url';
import chalk from 'chalk';

export default class ReactNativeCrudGenerator {
templates = {};

constructor({hydraPrefix, templateDirectory}) {
const templatePathCommon = `${templateDirectory}/react-common/`;
const templatePath = `${templateDirectory}/react/`;

this.hydraPrefix = hydraPrefix;

// actions
this.registerTemplate(templatePathCommon, 'actions/foo/create.js');
this.registerTemplate(templatePathCommon, 'actions/foo/delete.js');
this.registerTemplate(templatePathCommon, 'actions/foo/list.js');
this.registerTemplate(templatePathCommon, 'actions/foo/update.js');
this.registerTemplate(templatePathCommon, 'actions/foo/show.js');

// api
this.registerTemplate(templatePathCommon, 'api/fooFetch.js');

// components
this.registerTemplate(templatePath, 'components/foo/Create.js');
this.registerTemplate(templatePath, 'components/foo/Form.js');
this.registerTemplate(templatePath, 'components/foo/index.js');
this.registerTemplate(templatePath, 'components/foo/List.js');
this.registerTemplate(templatePath, 'components/foo/Update.js');
this.registerTemplate(templatePath, 'components/foo/Show.js');

// reducers
this.registerTemplate(templatePathCommon, 'reducers/foo/create.js');
this.registerTemplate(templatePathCommon, 'reducers//foo/delete.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/index.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/list.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/update.js');
this.registerTemplate(templatePathCommon, 'reducers/foo/show.js');

// entrypoint
this.registerTemplate(templatePathCommon, 'api/_entrypoint.js');
}

registerTemplate(templatePath, path) {
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath+path).toString());
}

help(resource) {
const titleLc = resource.title.toLowerCase()

console.log('Code for the "%s" resource type has been generated!', resource.title);
console.log('Paste the following definitions in your application configuration:');
console.log(chalk.green(`
// import reducers
import ${titleLc} from './reducers/${titleLc}/';
<<<<<<< HEAD
// Add the reducer
combineReducers(${titleLc},{/* ... */}),
=======

// Add the reducer
combineReducers(${titleLc},{/* ... */}),

>>>>>>> 3d43b1f377ef84d790d6022c6f61df5301c28246
`));
}

generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const titleUcFirst = resource.title.charAt(0).toUpperCase() + resource.title.slice(1);

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields: resource.readableFields,
formFields: this.buildFields(resource.writableFields),
hydraPrefix: this.hydraPrefix,
titleUcFirst
};


// Create directories
// These directories may already exist
mkdirp.sync(`${dir}/api`);

this.createDir(`${dir}/actions/${lc}`);
this.createDir(`${dir}/components/${lc}`);
this.createDir(`${dir}/reducers/${lc}`);

// actions
this.createFile('actions/foo/create.js', `${dir}/actions/${lc}/create.js`, context);
this.createFile('actions/foo/delete.js', `${dir}/actions/${lc}/delete.js`, context);
this.createFile('actions/foo/list.js', `${dir}/actions/${lc}/list.js`, context);
this.createFile('actions/foo/update.js', `${dir}/actions/${lc}/update.js`, context);
this.createFile('actions/foo/show.js', `${dir}/actions/${lc}/show.js`, context);

// api
this.createFile('api/fooFetch.js', `${dir}/api/${lc}Fetch.js`, context);

// components
this.createFile('components/foo/Create.js', `${dir}/components/${lc}/Create.js`, context);
this.createFile('components/foo/Form.js', `${dir}/components/${lc}/Form.js`, context);
this.createFile('components/foo/index.js', `${dir}/components/${lc}/index.js`, context);
this.createFile('components/foo/List.js', `${dir}/components/${lc}/List.js`, context);
this.createFile('components/foo/Update.js', `${dir}/components/${lc}/Update.js`, context);
this.createFile('components/foo/Show.js', `${dir}/components/${lc}/Show.js`, context);

// reducers
this.createFile('reducers/foo/create.js', `${dir}/reducers/${lc}/create.js`, context);
this.createFile('reducers//foo/delete.js', `${dir}/reducers/${lc}/delete.js`, context);
this.createFile('reducers/foo/index.js', `${dir}/reducers/${lc}/index.js`, context);
this.createFile('reducers/foo/list.js', `${dir}/reducers/${lc}/list.js`, context);
this.createFile('reducers/foo/update.js', `${dir}/reducers/${lc}/update.js`, context);
this.createFile('reducers/foo/show.js', `${dir}/reducers/${lc}/show.js`, context);
}

entrypoint(apiEntry, dir) {
const url = urlapi.parse(apiEntry);
const {protocol, host, port, pathname} = url;
const hostUrl = `${protocol}//${host}${port ? `:${port}` : ''}`;

const context = {
host: hostUrl,
path: pathname
}

this.createFile('api/_entrypoint.js', `${dir}/api/_entrypoint.js`, context);
}

getInputTypeFromField(field) {
switch (field.id) {
case 'http://schema.org/email':
return {type: 'email'};

case 'http://schema.org/url':
return {type: 'url'};
}

switch (field.range) {
case 'http://www.w3.org/2001/XMLSchema#integer':
return {type: 'number'};

case 'http://www.w3.org/2001/XMLSchema#decimal':
return {type: 'number', step: '0.1'};

case 'http://www.w3.org/2001/XMLSchema#boolean':
return {type: 'checkbox'};

case 'http://www.w3.org/2001/XMLSchema#date':
return {type: 'date'};

case 'http://www.w3.org/2001/XMLSchema#time':
return {type: 'time'};

default:
return {type: 'text'};
}
}

buildFields(apiFields) {
let fields = [];
for (let apiField of apiFields) {
let field = this.getInputTypeFromField(apiField);
field.required = apiField.required;
field.name = apiField.name;
field.description = apiField.description.replace(/"/g, "'"); // fix for Form placeholder description

fields.push(field)
}

return fields;
}

createDir(dir) {
if (fs.existsSync(dir)) throw new Error(`The directory "${dir}" already exists`);
mkdirp.sync(dir);
}

createFile(template, dest, context) {
fs.writeFileSync(dest, this.templates[template](context));
}
}
51 changes: 51 additions & 0 deletions src/generators/ReactNativeCrudGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Api from 'api-doc-parser/lib/Api';
import Resource from 'api-doc-parser/lib/Resource';
import Field from 'api-doc-parser/lib/Field';
import fs from 'fs';
import tmp from 'tmp';
import ReactNativeCrudGenerator from './ReactNativeCrudGenerator';


test('Generate a React app', () => {
const generator = new ReactNativeCrudGenerator({hydraPrefix: 'hydra:', templateDirectory: `${__dirname}/../../templates`});
const tmpobj = tmp.dirSync({unsafeCleanup: true});

const fields = [new Field('bar', {
id: 'http://schema.org/url',
range: 'http://www.w3.org/2001/XMLSchema#string',
reference: null,
required: true,
description: 'An URL'
})];
const resource = new Resource('abc', 'http://example.com/foos', {
id: 'foo',
title: 'Foo',
readableFields: fields,
writableFields: fields
});
const api = new Api('http://example.com', {
title: 'My API',
resources: [resource]
});
generator.generate(api, resource, tmpobj.name);

expect(fs.existsSync(tmpobj.name+'/actions/abc/create.js'), true);
expect(fs.existsSync(tmpobj.name+'/actions/abc/delete.js'), true);
expect(fs.existsSync(tmpobj.name+'/actions/abc/list.js'), true);
expect(fs.existsSync(tmpobj.name+'/actions/abc/update.js'), true);

expect(fs.existsSync(tmpobj.name+'/api/abcFetch.js'), true);

expect(fs.existsSync(tmpobj.name+'/components/abc/Create.js'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/Form.js'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/List.js'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/Update.js'), true);

expect(fs.existsSync(tmpobj.name+'/reducers/abc/create.js'), true);
expect(fs.existsSync(tmpobj.name+'/reducers/abc/delete.js'), true);
expect(fs.existsSync(tmpobj.name+'/reducers/abc/index.js'), true);
expect(fs.existsSync(tmpobj.name+'/reducers/abc/list.js'), true);
expect(fs.existsSync(tmpobj.name+'/reducers/abc/update.js'), true);

tmpobj.removeCallback();
});
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ program
.usage('entrypoint outputDirectory')
.option('-r, --resource [resourceName]', 'Generate CRUD for the given resource')
.option('-p, --hydra-prefix [hydraPrefix]', 'The hydra prefix used by the API', 'hydra:')
.option('-g, --generator [generator]', 'The generator to use, one of "react", "angular" etc.', 'react')
.option('-g, --generator [generator]', 'The generator to use, one of "react", "react-native", "angular", etc.', 'react')
.option('-t, --template-directory [templateDirectory]', 'The templates directory base to use. Final directory will be ${templateDirectory}/${generator}', `${__dirname}/../templates/`)
.parse(process.argv);

Expand Down
File renamed without changes.
48 changes: 48 additions & 0 deletions templates/react-native/components/foo/Create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {Component} from 'react';
import {ScrollView, Text} from 'react-native';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import Form from './Form';
import { create, loading, error } from '../../actions/{{{ lc }}}/create';

class Create extends Component {
static propTypes = {
error: PropTypes.string,
loading: PropTypes.bool.isRequired,
created: PropTypes.object,
create: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
};

componentWillUnmount() {
this.props.reset();
}

render() {
return (
<ScrollView>
<Text>Create component {{lc}}</Text>
</ScrollView>
);
}
}

const mapStateToProps = (state) => {
return {
created: state.{{{ lc }}}.create.created,
error: state.{{{ lc }}}.create.error,
loading: state.{{{ lc }}}.create.loading,
};
};

const mapDispatchToProps = (dispatch) => {
return {
create: values => dispatch(create(values)),
reset: () => {
dispatch(loading(false));
dispatch(error(null));
},
};
};

export default connect(mapStateToProps, mapDispatchToProps)(Create);
Loading