generator-swiftserver
Version:
Generator for Kitura REST webservice servers
1,341 lines (1,225 loc) • 51.2 kB
JavaScript
/*
* Copyright IBM Corporation 2016
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
var debug = require('debug')('generator-swiftserver:refresh')
var Generator = require('yeoman-generator')
var chalk = require('chalk')
var YAML = require('js-yaml')
var handlebars = require('handlebars')
var path = require('path')
var fs = require('fs')
var Handlebars = require('../lib/handlebars.js')
var actions = require('../lib/actions')
var helpers = require('../lib/helpers')
var swaggerize = require('ibm-openapi-support')
module.exports = Generator.extend({
constructor: function () {
Generator.apply(this, arguments)
// Allow the user to specify where the specification file is
this.option('specfile', {
desc: 'The location of the specification file.',
required: false,
hide: true,
type: String
})
this.option('spec', {
desc: 'The specification in a JSON format.',
required: false,
hide: true,
type: String
})
this.fs.copyHbs = (from, to, params) => {
var template = handlebars.compile(this.fs.read(from))
this.fs.write(to, template(params))
}
},
// Check if the given file exists, print a log message if it does and execute
// the provided callback function if it does not.
//
// The given file is specified relative to the destination root as a string or
// an array of path segments to be joined.
//
// The callback function is passed the absolute filepath of the given file.
//
// @param {(string|string[])} fileInProject - filepath of file to check relative to destination root,
// if an array is provided, the elements will be joined with
// the path separator
// @param {ifNotExistsInProjectCallback} cb - callback to be executed if file to check does not exist
//
// @callback ifNotExistsInProjectCallback
// @param {string} filepath - absolute filepath of file to check
_ifNotExistsInProject: function (fileInProject, cb) {
var filepath
if (typeof (fileInProject) === 'string') {
filepath = this.destinationPath(fileInProject)
} else {
filepath = this.destinationPath.apply(this, fileInProject)
}
if (this.fs.exists(this.destinationPath(filepath))) {
const relativeFilepath = path.relative(this.destinationRoot(), filepath)
this.log(chalk.cyan(' exists ') + relativeFilepath)
} else {
cb.call(this, filepath)
}
},
_writeHandlebarsFile: function (templateFile, destinationFile, data) {
let template = this.fs.read(this.templatePath(templateFile))
let compiledTemplate = Handlebars.compile(template)
let output = compiledTemplate(data)
this.fs.write(this.destinationPath(destinationFile), output)
},
initializing: {
readSpec: function () {
this.generatorVersion = require('../package.json').version
this.existingProject = false
if (this.options.specfile) {
debug('attempting to read the spec from file')
try {
this.spec = this.fs.readJSON(this.options.specfile)
this.existingProject = false
} catch (err) {
this.env.error(chalk.red(err))
}
}
if (this.options.spec) {
debug('attempting to read the spec from cli')
try {
this.spec = JSON.parse(this.options.spec)
} catch (err) {
this.env.error(chalk.red(err))
}
}
// Getting an object from the generators
if (this.options.specObj) {
this.spec = this.options.specObj
}
// If we haven't receieved some sort of spec we attempt to read the spec.json
if (!this.spec) {
try {
this.spec = this.fs.readJSON(this.destinationPath('spec.json'))
this.existingProject = true
} catch (err) {
this.env.error(chalk.red('Cannot read the spec.json: ', err))
}
}
if (!this.spec) {
this.env.error(chalk.red('No specification for this project'))
}
// App type
if (!this.spec.appType) {
this.env.error(chalk.red('Property appType is missing from the specification'))
}
if (['crud', 'scaffold'].indexOf(this.spec.appType) === -1) {
this.env.error(chalk.red(`Property appType is invalid: ${this.spec.appType}`))
}
this.appType = this.spec.appType
this.repoType = this.spec.repoType || 'link'
// App name
if (this.spec.appName) {
this.projectName = this.spec.appName
} else {
this.env.error(chalk.red('Property appName is missing from the specification'))
}
// Bluemix configuration
// Ensure minimal default bluemix properties are set
this.bluemix = this.spec.bluemix || {}
this.bluemix.backendPlatform = this.bluemix.backendPlatform || 'SWIFT'
this.bluemix.name = this.bluemix.name || this.projectName
this.bluemix.server = this.bluemix.server || {}
this.bluemix.server.name = this.bluemix.server.name || this.bluemix.name
this.bluemix.server.env = this.bluemix.server.env || {}
// StarterOptions configuration for Usecase enablement
this.starterOptions = this.spec.starterOptions || undefined
function isServiceProperty (value) {
if (Array.isArray(value)) {
// All elements of the service array must have a serviceInfo property
return value.filter(element => element.serviceInfo).length === value.length
}
return !!value.serviceInfo
}
function getServiceProperties (bluemixObject) {
var servicePropNames = Object.keys(bluemixObject).filter(prop => isServiceProperty(bluemixObject[prop]))
var serviceProperties = servicePropNames.reduce((memo, servicePropName) => {
return Object.assign(memo, { [servicePropName]: bluemixObject[servicePropName] })
}, {})
return serviceProperties
}
this.services = getServiceProperties(this.bluemix)
Object.keys(this.services).forEach(serviceType => {
var serviceOrServices = this.services[serviceType]
if (Array.isArray(serviceOrServices)) {
serviceOrServices.forEach((service, index) => {
var updatedService = helpers.sanitizeServiceAndFillInDefaults(serviceType, service)
helpers.validateServiceFields(serviceType, updatedService)
this.bluemix[serviceType][index] = updatedService
this.services[serviceType][index] = updatedService
})
} else {
var updatedService = helpers.sanitizeServiceAndFillInDefaults(serviceType, serviceOrServices)
helpers.validateServiceFields(serviceType, updatedService)
this.bluemix[serviceType] = updatedService
this.services[serviceType] = updatedService
}
})
if (typeof (this.bluemix.openApiServers) === 'object') {
this.openApiServers = this.bluemix.openApiServers
}
// Docker configuration
this.docker = (this.spec.docker === true)
// OpenAPI configuration
this.openapi = (this.spec.openapi === true)
// Usecase configuration
this.usecase = (this.spec.usecase === true)
// Web configuration
this.web = (this.spec.web === true)
// Example endpoints
this.exampleEndpoints = (this.spec.exampleEndpoints === true)
// Health endpoint
this.healthcheck = (typeof this.spec.healthcheck === 'undefined') ? true : this.spec.healthcheck
// Generation of example endpoints from the productSwagger.yaml example.
if (this.spec.fromSwagger && typeof (this.spec.fromSwagger) === 'string') {
this.openApiFileOrUrl = this.spec.fromSwagger
this.generateCodableRoutes = this.spec.generateCodableRoutes
}
if (this.exampleEndpoints) {
if (this.openApiFileOrUrl) {
this.env.error('Only one of: swagger file and example endpoints allowed')
}
this.openApiFileOrUrl = this.templatePath('common', 'productSwagger.yaml')
}
// Swagger file paths for server SDKs
this.serverSwaggerFiles = this.spec.serverSwaggerFiles || []
// Swagger hosting
this.hostSwagger = (this.spec.hostSwagger === true)
// Swagger UI
this.swaggerUI = (this.spec.swaggerUI === true)
// Metrics
this.metrics = (this.spec.metrics === true || undefined)
// Autoscaling implies monitoring
if (this.bluemix.autoscaling) {
this.metrics = true
}
// SwaggerUI imples web and hostSwagger
if (this.swaggerUI) {
this.hostSwagger = true
this.web = true
}
// CRUD generation implies SwaggerUI
if (this.appType === 'crud') {
this.hostSwagger = true
this.swaggerUI = true
this.web = true
}
// Define health-check-type and health-check-http-endpoint
/* if (this.healthcheck) {
this.bluemix.server['health-check-type'] = 'http'
this.bluemix.server['health-check-http-endpoint'] = '/health'
} */
// Define OPENAPI_SPEC
if (this.hostSwagger) {
this.bluemix.server.env.OPENAPI_SPEC = '"/swagger/api"'
}
// Set the names of the modules
this.generatedModule = 'Generated'
this.applicationModule = 'Application'
this.executableModule = this.projectName
// Target dependencies to add to the applicationModule
this.sdkTargets = []
// Files or folders to be ignored in a git repo
this.itemsToIgnore = []
// Package dependencies to add to Package.swift
// eg this.dependencies.push('.package(url: "https://github.com/IBM-Swift/Kitura.git", .upToNextMinor(from : "1.7.0")),')
this.dependencies = []
// Module Dependencies to add to Package.swift
this.modules = []
// Initialization code to add to Application.swift by code block
// eg this.appInitCode.services.push('try initializeServiceCloudant()')
this.appInitCode = {
metrics: undefined,
openapi: undefined,
capabilities: [],
services: [],
service_imports: [],
service_variables: [],
middlewares: [],
endpoints: []
}
if (this.web || this.usecase) this.appInitCode.middlewares.push('router.all(middleware: StaticFileServer())')
if (this.appType === 'crud') {
this.appInitCode.endpoints.push('try initializeCRUDResources(cloudEnv: cloudEnv, router: router)')
this.dependencies.push('.package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", from: "17.0.0"),')
}
if (this.metrics) {
this.modules.push('"SwiftMetrics"')
this.appInitCode.metrics = 'initializeMetrics(router: router)'
this.dependencies.push('.package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.0.0"),')
}
if (this.openapi) {
this.appInitCode.openapi = 'KituraOpenAPI.addEndpoints(to: router)'
this.dependencies.push('.package(url: "https://github.com/IBM-Swift/Kitura-OpenAPI.git", from: "1.0.0"),')
this.modules.push('"KituraOpenAPI"')
}
if (this.usecase) {
this.appInitCode.endpoints.push('initializeAppRoutes(app: self)')
}
},
ensureGeneratorIsCompatibleWithProject: function () {
if (!this.existingProject) return
var generatorMajorVersion = this.generatorVersion.split('.')[0]
var projectGeneratedWithVersion = this.config.get('version')
if (!projectGeneratedWithVersion) {
this.env.error(`Project was generated with an incompatible version of the generator (project was generated with an unknown version, current generator is v${generatorMajorVersion})`)
}
// Ensure generator major version match
// TODO Use node-semver? Strip leading non-digits?
var projectGeneratedWithMajorVersion = projectGeneratedWithVersion.split('.')[0]
if (projectGeneratedWithMajorVersion !== generatorMajorVersion) {
this.env.error(`Project was generated with an incompatible version of the generator (project was generated with v${projectGeneratedWithMajorVersion}, current generator is v${generatorMajorVersion})`)
}
},
setDestinationRootFromSpec: function () {
if (!this.options.destinationSet && !this.existingProject) {
// Check if we have a directory specified, else use the default one
this.destinationRoot(path.resolve(this.spec.appDir || 'swiftserver'))
}
},
ensureInProject: function () {
if (this.existingProject) {
actions.ensureInProject.call(this)
} else {
actions.ensureEmptyDirectory.call(this)
}
},
readModels: function () {
if (this.appType !== 'crud') return
// Start with the models from the spec
var modelMap = {}
if (this.spec.models) {
this.spec.models.forEach((model) => {
if (model.name) {
modelMap[model.name] = model
} else {
this.log('Failed to process model in spec: model name missing')
}
})
}
// Update the spec with any changes from the model .json files
try {
var modelFiles = fs.readdirSync(this.destinationPath('models'))
.filter((name) => name.endsWith('.json'))
modelFiles.forEach(function (modelFile) {
try {
debug('reading model json:', this.destinationPath('models', modelFile))
var modelJSON = fs.readFileSync(this.destinationPath('models', modelFile))
var model = JSON.parse(modelJSON)
// Only add models if they aren't being modified/added
if (model.name) {
modelMap[model.name] = model
} else {
this.log(`Failed to process model file ${modelFile}: model name missing`)
}
} catch (_) {
// Failed to read model file
this.log(`Failed to process model file ${modelFile}`)
}
}.bind(this))
if (modelFiles.length === 0) {
debug('no files in the model directory')
}
} catch (_) {
// No models directory
debug(this.destinationPath('models'), 'directory does not exist')
}
// Add any models we need to update
if (this.options.model) {
if (this.options.model.name) {
modelMap[this.options.model.name] = this.options.model
} else {
this.env.error(chalk.red('Failed to update model: name missing'))
}
}
this.models = Object.keys(modelMap).map((modelName) => modelMap[modelName])
},
loadProjectInfo: function () {
// TODO(tunniclm): Improve how we set these values
this.projectVersion = '1.0.0'
}
},
configuring: function () {
if (this.existingProject) return
this.composeWith(require.resolve('generator-ibm-service-enablement'), {
quiet: true,
bluemix: JSON.stringify(this.bluemix),
parentContext: {
injectIntoApplication: options => {
if (options.capability) this.appInitCode.capabilities.push(options.capability)
if (options.service) this.appInitCode.services.push(options.service)
if (options.service_import) this.appInitCode.service_imports.push(options.service_import)
if (options.service_variable) this.appInitCode.service_variables.push(options.service_variable)
if (options.endpoint) this.appInitCode.endpoints.push(options.endpoint)
if (options.middleware) this.appInitCode.middlewares.push(options.middleware)
},
injectDependency: dependency => { this.dependencies.push(dependency) },
injectModules: modules => {
this.modules.push(modules)
}
}
})
},
buildProduct: function () {
if (!this.options.apic) return
this.product = {
'product': '1.0.0',
'info': {
'name': this.projectName,
'title': this.projectName,
'version': this.projectVersion
},
'apis': {
[this.projectName]: {
'$ref': this.projectName + '.yaml'
}
},
'visibility': {
'view': {
'type': 'public'
},
'subscribe': {
'type': 'authenticated'
}
},
'plans': {
'default': {
'title': 'Default Plan',
'description': 'Default Plan',
'approval': false,
'rate-limit': {
'value': '100/hour',
'hard-limit': false
}
}
}
}
},
buildSwagger: function () {
if (this.appType !== 'crud') return
var swagger = {
'swagger': '2.0',
'info': {
'version': this.projectVersion,
'title': this.projectName
},
'schemes': ['http'],
'basePath': '/api',
'consumes': ['application/json'],
'produces': ['application/json'],
'paths': {},
'definitions': {}
}
if (this.options.apic) {
swagger['info']['x-ibm-name'] = this.projectName
swagger['schemes'] = ['https']
swagger['host'] = '$(catalog.host)'
swagger['securityDefinitions'] = {
'clientIdHeader': {
'type': 'apiKey',
'in': 'header',
'name': 'X-IBM-Client-Id'
},
'clientSecretHeader': {
'type': 'apiKey',
'in': 'header',
'name': 'X-IBM-Client-Secret'
}
}
swagger['security'] = [{
'clientIdHeader': [],
'clientSecretHeader': []
}]
swagger['x-ibm-configuration'] = {
'testable': true,
'enforced': true,
'cors': { 'enabled': true },
'catalogs': {
'apic-dev': {
'properties': {
'runtime-url': '$(TARGET_URL)'
}
},
'sb': {
'properties': {
'runtime-url': 'http://localhost:4001'
}
}
},
'assembly': {
'execute': [{
'invoke': {
'target-url': '$(runtime-url)$(request.path)$(request.search)'
}
}]
}
}
}
this.models.forEach(function (model) {
var modelName = model['name']
var modelNamePlural = model['plural']
var collectivePath = `/${modelNamePlural}`
var singlePath = `/${modelNamePlural}/{id}`
// tunniclm: Generate definitions
var swaggerProperties = {}
var requiredProperties = []
for (var propName in model['properties']) {
swaggerProperties[propName] = {
'type': model['properties'][propName]['type']
}
if (typeof model['properties'][propName]['format'] !== 'undefined') {
swaggerProperties[propName]['format'] = model['properties'][propName]['format']
}
if (model['properties'][propName]['required'] === true) {
requiredProperties.push(propName)
}
}
swagger['definitions'][modelName] = {
'properties': swaggerProperties,
'additionalProperties': false
}
if (requiredProperties.length > 0) {
swagger['definitions'][modelName]['required'] = requiredProperties
}
// tunniclm: Generate paths
swagger['paths'][singlePath] = {
'get': {
'tags': [modelName],
'summary': 'Find a model instance by {{id}}',
'operationId': modelName + '.findOne',
'parameters': [
{
'name': 'id',
'in': 'path',
'description': 'Model id',
'required': true,
'type': 'string',
'format': 'JSON'
}
],
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'$ref': '#/definitions/' + modelName
}
}
},
'deprecated': false
},
'put': {
'tags': [modelName],
'summary': 'Put attributes for a model instance and persist it',
'operationId': modelName + '.replace',
'parameters': [
{
'name': 'data',
'in': 'body',
'description': 'An object of model property name/value pairs',
'required': true,
'schema': {
'$ref': '#/definitions/' + modelName
}
},
{
'name': 'id',
'in': 'path',
'description': 'Model id',
'required': true,
'type': 'string',
'format': 'JSON'
}
],
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'$ref': '#/definitions/' + modelName
}
}
},
'deprecated': false
},
'patch': {
'tags': [modelName],
'summary': 'Patch attributes for a model instance and persist it',
'operationId': modelName + '.update',
'parameters': [
{
'name': 'data',
'in': 'body',
'description': 'An object of model property name/value pairs',
'required': true,
'schema': {
'$ref': '#/definitions/' + modelName
}
},
{
'name': 'id',
'in': 'path',
'description': 'Model id',
'required': true,
'type': 'string',
'format': 'JSON'
}
],
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'$ref': '#/definitions/' + modelName
}
}
},
'deprecated': false
},
'delete': {
'tags': [modelName],
'summary': 'Delete a model instance by {{id}}',
'operationId': modelName + '.delete',
'parameters': [
{
'name': 'id',
'in': 'path',
'description': 'Model id',
'required': true,
'type': 'string',
'format': 'JSON'
}
],
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'type': 'object'
}
}
},
'deprecated': false
}
}
swagger['paths'][collectivePath] = {
'post': {
'tags': [modelName],
'summary': 'Create a new instance of the model and persist it',
'operationId': modelName + '.create',
'parameters': [
{
'name': 'data',
'in': 'body',
'description': 'Model instance data',
'required': true,
'schema': {
'$ref': '#/definitions/' + modelName
}
}
],
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'$ref': '#/definitions/' + modelName
}
}
},
'deprecated': false
},
'get': {
'tags': [modelName],
'summary': 'Find all instances of the model',
'operationId': modelName + '.findAll',
'responses': {
'200': {
'description': 'Request was successful',
'schema': {
'type': 'array',
'items': {
'$ref': '#/definitions/' + modelName
}
}
}
},
'deprecated': false
},
'delete': {
'tags': [modelName],
'summary': 'Delete all instances of the model',
'operationId': modelName + '.deleteAll',
'responses': {
'200': {
'description': 'Request was successful'
}
},
'deprecated': false
}
}
})
this.swagger = swagger
},
loadOpenApiDocument: function () {
this.openApiDocumentBytes = this.openApiServers && this.openApiServers[0] && this.openApiServers[0].spec
if (!this.openApiFileOrUrl && !this.openApiDocumentBytes) {
debug('neither bluemix openApiServers or fromSwagger options have been set')
return
}
if (this.openApiFileOrUrl && this.openApiDocumentBytes) {
debug('both bluemix openApiServers and fromSwagger options have been set')
throw new Error('cannot handle two sources of API definition')
}
if (this.openApiFileOrUrl) {
return swaggerize.loadAsync(this.openApiFileOrUrl, this.fs)
.then(loaded => {
this.openApiDocumentBytes = loaded
})
}
},
parseOpenApiDocument: function () {
let formatters = {
'pathFormatter': helpers.reformatPathToSwiftKitura,
'resourceFormatter': helpers.resourceNameFromPath,
'typeFormatter': helpers.swiftFromSwaggerType
}
if (this.openApiDocumentBytes) {
return swaggerize.parse(this.openApiDocumentBytes, formatters)
.then(response => {
this.loadedApi = response.loaded
this.parsedSwagger = response.parsed
// mangle the route name to allow the renaming of the default route.
Object.keys(this.parsedSwagger.resources).forEach(resource => {
debug('RESOURCENAME:', resource)
if (resource.endsWith('*')) {
this.parsedSwagger.resources[resource]['generatedName'] = resource.replace(/\*$/, 'Default')
} else {
this.parsedSwagger.resources[resource]['generatedName'] = resource + '_'
}
// if params are present then this is a codable route
let routeDetails = this.parsedSwagger.resources[resource]
routeDetails.forEach(detail => {
// get the method variant (needed by the template).
detail.variant = helpers.routeMethodVariant(detail)
// set the handler name.
detail.handlerName = helpers.handlerName(detail)
if (!helpers.supportedMethod(detail.method)) {
// any unsupported method types are non-codable, so they will always generate a raw route.
detail.codable = false
} else if (detail.variant === 'id' && detail.idtype === undefined) {
// the method variant demands an id, but none was provided on the path.
detail.codable = false
} else if (detail.route.match(/:[^/]+\//)) {
// any route with a param embedded within it cannot be codable, so it will always generate a raw route.
detail.codable = false
} else if (detail.params && detail.params.length > 0) {
// to generate a codable handler name, use the
// model name found in the first parameter.
detail.param = detail.params[0]['model']
// modify the route by removing the end parameter.
detail.route = detail.route.replace(/:.+$/, '')
// and make codable.
detail.codable = true
} else if (detail.responses && detail.responses.length > 0) {
// to generate a codable handler name, use the
// model name found in the first response.
detail.response = detail.responses[0]['model']
if (detail.method === 'get' || detail.method === 'delete') {
// for delete and get methods, remove any parts of the route
// following the first parameter on the route (":...")
let argsearch = detail.route.match(/:.+$/)
if (argsearch) {
detail.route = detail.route.substring(0, argsearch['index'])
}
}
// and make codable.
detail.codable = true
} else {
// no params or responses.
// if this is a delete, it can be codable, otherwise it is not codable.
// although deletes do not pass codable objects, they can still be called in the same way.
detail.codable = detail.method === 'delete'
}
})
})
// Declares a comparison function and sort the resources to get the codable ones first.
let cmp = function (a, b) {
let ca = a['codable']
let cb = b['codable']
if (ca === cb) return 0
if (ca && !cb) return -1
if (!ca && cb) return 1
}
Object.keys(this.parsedSwagger.resources).forEach(resource => {
this.parsedSwagger.resources[resource] = this.parsedSwagger.resources[resource].sort(cmp.bind(this))
})
})
.catch(err => {
if (this.openApiFileOrUrl) {
err.message = chalk.red('failed to parse:' + this.openApiFileOrUrl + ' ' + err.message)
} else {
err.message = chalk.red('failed to parse document from bluemix.openApiServers ' + err.message)
}
throw err
})
}
},
addEndpointInitCode: function () {
var endpointNames = []
if (this.healthcheck) {
this.modules.push('"Health"')
endpointNames.push('Health')
this.dependencies.push('.package(url: "https://github.com/IBM-Swift/Health.git", from: "1.0.0"),')
}
if (this.parsedSwagger && this.parsedSwagger.resources) {
var resourceNames = []
Object.keys(this.parsedSwagger.resources).forEach(resource => {
resourceNames.push(this.parsedSwagger.resources[resource].generatedName)
})
endpointNames = endpointNames.concat(resourceNames)
}
var initCodeForEndpoints = endpointNames.map(name => `initialize${name}Routes(app: self)`)
this.appInitCode.endpoints = this.appInitCode.endpoints.concat(initCodeForEndpoints)
if (this.hostSwagger) {
this.appInitCode.endpoints.push(`initializeSwaggerRoutes(app: self)`)
this.swaggerPath = `let swaggerPath = projectPath + "/definitions/${this.projectName}.yaml"`
}
// This endpoint needs to come last because it should have least precedence.
if (this.usecase) {
this.appInitCode.endpoints.push('initializeErrorRoutes(app: self)')
}
},
writing: {
createCommonFiles: function () {
// Check if we should create generator metadata files
if (!this.options.singleShot) {
// Root directory
this.config.defaults({ version: this.generatorVersion })
// Check if there is a .swiftservergenerator-project, create one if there isn't
this._ifNotExistsInProject('.swiftservergenerator-project', (filepath) => {
// NOTE(tunniclm): Write a zero-byte file to mark this as a valid project
// directory
this.fs.write(filepath, '')
})
}
// Check if there is a .gitignore, create one if there isn't
this._ifNotExistsInProject('.gitignore', (filepath) => {
this._writeHandlebarsFile('common/gitignore', filepath,
{ itemsToIgnore: this.itemsToIgnore }
)
})
this._ifNotExistsInProject('.swift-version', (filepath) => {
this.fs.copy(this.templatePath('common', 'swift-version'),
filepath)
})
this._ifNotExistsInProject('LICENSE', (filepath) => {
this.fs.copy(
this.templatePath('common', 'LICENSE_for_generated_code'),
filepath)
})
this._ifNotExistsInProject(['Sources', this.applicationModule, 'InitializationError.swift'], (filepath) => {
this.fs.copy(this.templatePath('common', 'InitializationError.swift'),
filepath)
})
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Logging.swift'], (filepath) => {
this.fs.copy(this.templatePath('common', 'Logging.swift'),
filepath)
})
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Application.swift'], (filepath) => {
this._writeHandlebarsFile('common/Application.swift', `Sources/${this.applicationModule}/Application.swift`,
{
appType: this.appType,
generatedModule: this.generatedModule,
appInitCode: this.appInitCode,
swaggerPath: this.swaggerPath,
web: this.web,
openapi: this.openapi,
healthcheck: this.healthcheck,
basepath: this.parsedSwagger && this.parsedSwagger.basepath
}
)
})
// Check if there is a spec.json, if there isn't create one
if (this.spec) {
this._ifNotExistsInProject('spec.json', (filepath) => {
this.fs.writeJSON(filepath, this.spec)
})
}
// Check if there is a index.js, create one if there isn't
if (this.options.apic) {
this._ifNotExistsInProject('index.js', (filepath) => {
this.fs.copy(this.templatePath('common', 'apic-node-wrapper.js'),
filepath)
})
}
this._ifNotExistsInProject(['Tests', this.applicationModule + 'Tests', 'RouteTests.swift'], (filepath) => {
this._writeHandlebarsFile('common/RouteTests.swift', filepath,
{ applicationModule: this.applicationModule }
)
})
this._ifNotExistsInProject(['Tests', 'LinuxMain.swift'], (filepath) => {
this._writeHandlebarsFile('common/LinuxMain.swift', filepath,
{ executableModule: this.executableModule, applicationModule: this.applicationModule }
)
})
if (this.metrics) {
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Metrics.swift'], (filepath) => {
this.fs.copy(
this.templatePath('common', 'Metrics.swift'),
filepath
)
})
}
if (this.healthcheck) {
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Routes', 'HealthRoutes.swift'], (filepath) => {
this.fs.copyTpl(
this.templatePath('common', 'HealthRoutes.swift'),
filepath
)
})
}
if (this.hostSwagger) {
this.fs.write(this.destinationPath('definitions', '.keep'), '')
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Routes', 'SwaggerRoutes.swift'], (filepath) => {
this.fs.copyTpl(
this.templatePath('common', 'SwaggerRoutes.swift'),
filepath
)
})
}
if (this.swaggerUI) {
this.fs.copy(
this.templatePath('common', 'swagger-ui/**/*'),
this.destinationPath('public', 'explorer')
)
this.fs.copy(
this.templatePath('common', 'NOTICES_for_generated_swaggerui'),
this.destinationPath('NOTICES.txt')
)
}
if (this.web) {
this.fs.write(this.destinationPath('public', '.keep'), '')
}
if (this.usecase) {
this.fs.copy(
this.templatePath('public'),
this.destinationPath('public')
)
this._ifNotExistsInProject(['Sources', this.applicationModule, 'Routes', 'ErrorRoutes.swift'], (filepath) => {
this.fs.copyTpl(
this.templatePath('common', 'ErrorRoutes.swift'),
filepath
)
})
}
if (this.appType !== 'crud') {
this._writeHandlebarsFile('common/README.scaffold.md', 'README.md',
{
appName: this.projectName,
executableName: this.executableModule,
chartName: helpers.sanitizeAppName(this.bluemix.name),
generatorVersion: this.generatorVersion,
web: this.web,
docker: this.docker,
hostSwagger: this.hostSwagger,
exampleEndpoints: this.exampleEndpoints,
metrics: this.metrics,
autoscaling: !!this.bluemix.autoscaling,
cloudant: this.bluemix.cloudant && this.bluemix.cloudant.length > 0,
redis: !!this.bluemix.redis,
appid: !!this.bluemix.appid,
conversation: !!this.bluemix.conversation,
alertNotification: !!this.bluemix.alertNotification,
push: !!this.bluemix.push
}
)
this.fs.write(this.destinationPath('Sources', this.applicationModule, 'Routes', '.keep'), '')
}
this._ifNotExistsInProject('iterative-dev.sh', (filepath) => {
this._writeHandlebarsFile('common/iterative-dev.sh', filepath,
{ executableModule: this.executableModule }
)
})
},
createFromSwagger: function () {
if (this.parsedSwagger) {
handlebars.registerHelper('swifttype', helpers.swiftFromSwaggerProperty)
handlebars.registerHelper('ifequal', helpers.ifequal)
Object.keys(this.parsedSwagger.resources).forEach(resource => {
// Generate routes
var generatedName = this.parsedSwagger.resources[resource].generatedName
debug('route:', this.parsedSwagger.resources[resource])
this.fs.copyHbs(
this.templatePath('fromswagger', 'Routes.swift.hbs'),
this.destinationPath('Sources', this.applicationModule, 'Routes', `${generatedName}Routes.swift`),
{
resource: generatedName,
routedetail: this.parsedSwagger.resources[resource],
hascodable: this.parsedSwagger.resources[resource].reduce((acc, val) => { return acc + (val.codable ? 1 : 0) }, 0) > 0,
basepath: this.parsedSwagger.basepath,
generatecodable: this.generateCodableRoutes
}
)
})
// Generate model structures
Object.keys(this.parsedSwagger.models).forEach(name => {
var model = this.parsedSwagger.models[name]
var fileName = helpers.capitalizeFirstLetter(name + '.swift')
debug('model:', model)
debug('fileName:', fileName)
if (!model.id) {
model.id = name
}
// For Array of items/models referenced as part of definitions, no need
// generate a model file.
if (model.type === 'array' && model.items) {
return
}
if (model.properties) {
debug('model.properties', model.properties)
Object.keys(model.properties).forEach(prop => {
if (model.properties[prop].$ref) {
model.properties[prop]['type'] = 'ref'
}
if (!model.required || (model.required && !helpers.arrayContains(prop, model.required))) {
model.properties[prop]['optional'] = '?'
}
if (model.properties[prop].description && model.properties[prop].description.length > 0) {
// model.properties[prop].description = '// ' + model.properties[prop].description
var comments = model.properties[prop].description.split('\n')
if (comments[comments.length - 1].length === 0) {
comments = comments.slice(0, comments.length - 1)
}
model.properties[prop].description = comments
}
})
this.fs.copyHbs(
this.templatePath('fromswagger', 'Model.swift.hbs'),
this.destinationPath('Sources', this.applicationModule, 'Models', fileName),
{
properties: model.properties,
model: name,
required: model.required || [],
id: name,
license: this.license
})
}
})
// Make the swagger available for the swaggerUI
var swaggerFilename = this.destinationPath('definitions', `${this.projectName}.yaml`)
this.fs.write(swaggerFilename, YAML.safeDump(this.loadedApi))
}
},
createCRUD: function () {
if (this.appType !== 'crud') return
if (!this.fs.exists(this.destinationPath('README.md'))) {
this.fs.copy(
this.templatePath('bluemix', 'README.md'),
this.destinationPath('README.md')
)
}
// Add the models to the spec
this.spec.models = this.models
this.fs.writeJSON(this.destinationPath('spec.json'), this.spec)
// Check if there is a models folder, create one if there isn't
if (!this.fs.exists(this.destinationPath('models', '.keep'))) {
this.fs.write(this.destinationPath('models', '.keep'), '')
}
this.models.forEach(function (model) {
var modelMetadataFilename = this.destinationPath('models', `${model.name}.json`)
this.fs.writeJSON(modelMetadataFilename, model, null, 2)
}.bind(this))
// Get the CRUD service for persistence
function getService (services, serviceName) {
var serviceDef = null
Object.keys(services).forEach(function (serviceType) {
if (serviceDef) return
var serviceOrServices = services[serviceType]
if (!Array.isArray(serviceOrServices)) serviceOrServices = [serviceOrServices]
serviceOrServices.forEach(function (service) {
if (service.serviceInfo.name && (service.serviceInfo.name === serviceName)) {
serviceDef = {
service: service,
type: serviceType
}
}
})
})
return serviceDef
}
var crudService
if (this.spec.crudservice) {
crudService = getService(this.services, this.spec.crudservice)
} else {
crudService = { type: '__memory__' }
}
this._writeHandlebarsFile('crud/CRUDResources.swift', `Sources/${this.generatedModule}/CRUDResources.swift`,
{models: this.models}
)
this._writeHandlebarsFile('crud/AdapterFactory.swift', `Sources/${this.generatedModule}/AdapterFactory.swift`,
{ models: this.models, crudService: crudService }
)
this.fs.copy(
this.templatePath('crud', 'AdapterError.swift'),
this.destinationPath('Sources', this.generatedModule, 'AdapterError.swift')
)
this.fs.copy(
this.templatePath('crud', 'ModelError.swift'),
this.destinationPath('Sources', this.generatedModule, 'ModelError.swift')
)
this.models.forEach(function (model) {
this._writeHandlebarsFile('crud/Resource.swift', `Sources/${this.generatedModule}/${model.classname}Resource.swift`,
{ model: model }
)
this._writeHandlebarsFile('crud/Adapter.swift', `Sources/${this.generatedModule}/${model.classname}Adapter.swift`,
{ model: model }
)
switch (crudService.type) {
case 'cloudant':
this._writeHandlebarsFile('crud/CloudantAdapter.swift', `Sources/${this.generatedModule}/${model.classname}CloudantAdapter.swift`,
{ model: model }
)
break
case '__memory__':
this._writeHandlebarsFile('crud/MemoryAdapter.swift', `Sources/${this.generatedModule}/${model.classname}MemoryAdapter.swift`,
{ model: model }
)
break
}
function optional (propertyName) {
var required = (model.properties[propertyName].required === true)
var identifier = (model.properties[propertyName].id === true)
return !required || identifier
}
function convertJSTypeToSwiftyJSONType (jsType) {
switch (jsType) {
case 'boolean': return 'bool'
case 'object': return 'dictionary'
default: return jsType
}
}
function convertJSTypeToSwiftyJSONProperty (jsType) {
switch (jsType) {
case 'boolean': return 'bool'
case 'array': return 'arrayObject'
case 'object': return 'dictionaryObject'
default: return jsType
}
}
var propertyInfos = Object.keys(model.properties).map(
(propertyName) => ({
name: propertyName,
jsType: model.properties[propertyName].type,
swiftyJSONType: convertJSTypeToSwiftyJSONType(model.properties[propertyName].type),
swiftyJSONProperty: convertJSTypeToSwiftyJSONProperty(model.properties[propertyName].type),
swiftType: helpers.convertJSTypeToSwift(model.properties[propertyName].type,
optional(propertyName)),
optional: optional(propertyName)
})
)
var defaultValueClause = ''
propertyInfos.filter((info) => info.optional).forEach(function (info) {
if (typeof (model.properties[info.name].default) !== 'undefined') {
var swiftDefaultLiteral = helpers.convertJSDefaultValueToSwift(model.properties[info.name].default)
defaultValueClause = ' ?? ' + swiftDefaultLiteral
}
})
const noInfoFilter = propertyInfos.filter(info => !info.optional)
const infoFilter = propertyInfos.filter(info => info.optional)
this._writeHandlebarsFile('crud/Model.swift', `Sources/${this.generatedModule}/${model.classname}.swift`,
{ model: model, propertyInfos: propertyInfos, helpers: helpers, infoFilter: infoFilter, noInfoFilter: noInfoFilter, defaultValueClause: defaultValueClause }
)
}.bind(this))
if (this.product) {
var productRelativeFilename = path.join('definitions', `${this.projectName}-product.yaml`)
var productFilename = this.destinationPath(productRelativeFilename)
if (this.fs.exists(productFilename)) {
// Do not overwrite this file if it already exists
this.log(chalk.red('exists, not modifying ') + productRelativeFilename)
} else {
this.fs.write(productFilename, YAML.safeDump(this.product))
}
}
if (this.swagger) {
var swaggerFilename = this.destinationPath('definitions', `${this.projectName}.yaml`)
this.conflicter.force = true
this.fs.write(swaggerFilename, YAML.safeDump(this.swagger))
// Append items to .gitignore that may have been added through model gen process
var gitignoreFile = this.fs.read(this.destinationPath('.gitignore'))
for (var item in this.itemsToIgnore) {
// Ensure we only add unique values
if (gitignoreFile.indexOf(this.itemsToIgnore[item]) === -1) {
this.fs.append(
this.destinationPath('.gitignore'),
this.itemsToIgnore[item]
)
}
}
}
},
writeMainSwift: function () {
// Adding the main.swift file by searching for it in the folders
// and adding it if it is not there.
var foundMainSwift = false
if (fs.existsSync(this.destinationPath('Sources'))) {
// Read all the folders in the Sources directory
var folders = fs.readdirSync(this.destinationPath('Sources'))
// Read all the files in each folder
folders.forEach(function (folder) {
if (folder.startsWith('.')) return
if (!fs.statSync(this.destinationPath('Sources', folder)).isDirectory()) return
var files = fs.readdirSync(this.destinationPath('Sources', folder))
if (files.indexOf('main.swift') !== -1) {
foundMainSwift = true
}
}.bind(this))
}
if (!foundMainSwift) {
this._writeHandlebarsFile('common/main.swift', `Sources/${this.executableModule}/main.swift`,
{ applicationModule: this.applicationModule }
)
}
},
writeDockerFiles: function () {
if (!this.docker || this.existingProject) return
this.composeWith(require.resolve('generator-ibm-cloud-enablement/generators/dockertools'), { force: this.force, bluemix: this.bluemix })
},
writeBluemixDeploymentFiles: function () {
if (this.existingProject) return
// Go through the bluemix object and pull out the serviceInfo for each of
// the services to pass down to cloud enablement
this.bluemix.services = {}
Object.keys(this.services).forEach(prop => {
var services = Array.isArray(this.services[prop]) ? this.services[prop] : [this.services[prop]]
this.bluemix.services[prop] = services.map(service => service.serviceInfo)
})
this.composeWith(require.resolve('generator-ibm-cloud-enablement/generators/deployment'),
{ force: this.force,
bluemix: this.bluemix,
repoType: this.repoType,
deploymentRegion: this.spec.deploymentRegion,
deploymentOrg: this.spec.deploymentOrg,
deploymentSpace: this.spec.deploymentSpace,
toolchainName: this.spec.toolchainName
})
},
writeKubernetesFiles: function () {
if (!this.docker || this.existingProject) return
this.composeWith(require.resolve('generator-ibm-cloud-enablement/generators/kubernetes'), { force: this.force, bluemix: this.bluemix })
},
writeVSIFi