diff --git a/lib/CodeGenerator.js b/lib/CodeGenerator.js new file mode 100644 index 00000000..92c5bffb --- /dev/null +++ b/lib/CodeGenerator.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +// $FlowFixMe + +var _minimist = require('minimist'); + +var _minimist2 = _interopRequireDefault(_minimist); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _Procedure = require('./common/Procedure'); + +var _Procedure2 = _interopRequireDefault(_Procedure); + +var _Utils = require('./common/Utils'); + +var _Utils2 = _interopRequireDefault(_Utils); + +var _SpecFileLoader = require('./loaders/SpecFileLoader'); + +var _SpecFileLoader2 = _interopRequireDefault(_SpecFileLoader); + +var _AutoAddIdFieldForRootNodeProcessor = require('./processors/AutoAddIdFieldForRootNodeProcessor'); + +var _AutoAddIdFieldForRootNodeProcessor2 = _interopRequireDefault(_AutoAddIdFieldForRootNodeProcessor); + +var _CreationEndpointHackProcessor = require('./processors/CreationEndpointHackProcessor'); + +var _CreationEndpointHackProcessor2 = _interopRequireDefault(_CreationEndpointHackProcessor); + +var _FlaggingProcessor = require('./processors/FlaggingProcessor'); + +var _FlaggingProcessor2 = _interopRequireDefault(_FlaggingProcessor); + +var _LanguageSpecificProcessor = require('./processors/LanguageSpecificProcessor'); + +var _LanguageSpecificProcessor2 = _interopRequireDefault(_LanguageSpecificProcessor); + +var _NamingConventionProcessor = require('./processors/NamingConventionProcessor'); + +var _NamingConventionProcessor2 = _interopRequireDefault(_NamingConventionProcessor); + +var _NodeEndpointHackProcessor = require('./processors/NodeEndpointHackProcessor'); + +var _NodeEndpointHackProcessor2 = _interopRequireDefault(_NodeEndpointHackProcessor); + +var _NormalizationProcessor = require('./processors/NormalizationProcessor'); + +var _NormalizationProcessor2 = _interopRequireDefault(_NormalizationProcessor); + +var _ReferenceProcessor = require('./processors/ReferenceProcessor'); + +var _ReferenceProcessor2 = _interopRequireDefault(_ReferenceProcessor); + +var _SpecOverridingProcessor = require('./processors/SpecOverridingProcessor'); + +var _SpecOverridingProcessor2 = _interopRequireDefault(_SpecOverridingProcessor); + +var _DebugJsonRenderer = require('./renderers/DebugJsonRenderer'); + +var _DebugJsonRenderer2 = _interopRequireDefault(_DebugJsonRenderer); + +var _MustacheRenderer = require('./renderers/MustacheRenderer'); + +var _MustacheRenderer2 = _interopRequireDefault(_MustacheRenderer); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const procedure = new _Procedure2.default({ + loader: _SpecFileLoader2.default, + processors: [_SpecOverridingProcessor2.default, _AutoAddIdFieldForRootNodeProcessor2.default, _NodeEndpointHackProcessor2.default, _NormalizationProcessor2.default, _FlaggingProcessor2.default, _CreationEndpointHackProcessor2.default, _NamingConventionProcessor2.default, _ReferenceProcessor2.default, _LanguageSpecificProcessor2.default], + renderers: [_MustacheRenderer2.default, _DebugJsonRenderer2.default] +}); + +const args = (0, _minimist2.default)(process.argv.slice(2)); + +const language = args._[0]; +_Utils2.default.validateLanguage(language); + +const version = args.v || _Utils2.default.loadDefaultVersion(); +const sdk_version = _Utils2.default.loadDefaultSDKVersion(language); +const outputDir = args.o || _path2.default.resolve(__dirname, '../../', './sdk/servers/' + language + '/release'); +const cleandir = args.c ? args.c.split(',') : []; + +procedure.run(version, sdk_version, language, outputDir, cleandir); \ No newline at end of file diff --git a/lib/common/Procedure.js b/lib/common/Procedure.js new file mode 100644 index 00000000..c52b2baf --- /dev/null +++ b/lib/common/Procedure.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _types = require('./types'); + +class Procedure { + + constructor(props) { + this.loader = props.loader; + this.processors = props.processors; + this.renderers = props.renderers; + } + + run(version, sdk_version, language, outputDir, cleandir) { + const inputs = this.loader.load(version, sdk_version); + + const metadata = inputs.metadata; + metadata.language = language; + metadata.version = version; + metadata.sdk_version = sdk_version; + metadata.outputDir = outputDir; + metadata.cleandir = cleandir; + + let specs = inputs.specs; + this.processors.forEach(processor => { + specs = processor.process(specs, metadata); + }); + + this.renderers.forEach(renderer => { + renderer.render({ + APISpecs: specs.api_specs, + SDKConfig: { + api_version: metadata.version, + sdk_version: metadata.sdk_version, + api_version_num_only: metadata.version.replace('v', ''), + version: metadata.versionedFeaturesWithDepreciation + } + }, metadata.language || '', metadata.version, metadata.outputDir || '', metadata.cleandir || []); + }); + } +} + +exports.default = Procedure; \ No newline at end of file diff --git a/lib/common/Utils.js b/lib/common/Utils.js new file mode 100644 index 00000000..3720f39f --- /dev/null +++ b/lib/common/Utils.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict + */ + +'use strict'; + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _version_path = require('../../api_specs/version_path.json'); + +var _version_path2 = _interopRequireDefault(_version_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const Utils = { + // special signature that can mark a codegen file should be removed. + codeGenFileDepreciationSign: '@remove_depreciated_file@' + new Date().getTime().toString(), + + throwUsageError(message) { + console.log('Usage: codegen [-v api_version] [-o output_path] [-c folder_to_cleanup]'); + throw message; + }, + + validateLanguage(language) { + // @fb-only + 'java', 'nodejs', 'php', 'python', 'ruby']; + if (!language) { + this.throwUsageError('language is not specified! available languages: ' + supportedLanguages.join(', ')); + } + + if (supportedLanguages.indexOf(language) < 0) { + this.throwUsageError('unsupported language: ' + language + '! available languages: ' + supportedLanguages.join(', ')); + } + }, + + loadDefaultVersion() { + const fileName = 'api_specs/specs/version.txt'; + const filePath = _path2.default.resolve(__dirname, '..', '..', fileName); + return _fs2.default.readFileSync(filePath, 'utf8').trim(); + }, + + loadDefaultSDKVersion(language) { + const fileName = _version_path2.default[language]['base_path'] + '/' + _version_path2.default[language]['file_path']; + const filePath = _path2.default.resolve(__dirname, '..', '..', '..', fileName); + var array = _fs2.default.readFileSync(filePath, 'utf8').toString().split("\n"); + + for (let line of array) { + if (line.trim().startsWith(_version_path2.default[language]['line_starter'])) { + let match = line.match(/^.*?(\d+\.\d+\.\d+(\.\d+)?).*$/i); + if (match) { + return match[1]; + } + } + } + + throw 'Not able to find sdk version.'; + } +}; + +module.exports = Utils; \ No newline at end of file diff --git a/lib/common/types.js b/lib/common/types.js new file mode 100644 index 00000000..37de4207 --- /dev/null +++ b/lib/common/types.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; \ No newline at end of file diff --git a/lib/loaders/SpecFileLoader.js b/lib/loaders/SpecFileLoader.js new file mode 100644 index 00000000..4bee05e1 --- /dev/null +++ b/lib/loaders/SpecFileLoader.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _Utils = require('./Utils'); + +var _Utils2 = _interopRequireDefault(_Utils); + +var _versions = require('../../api_specs/versions.json'); + +var _versions2 = _interopRequireDefault(_versions); + +var _Utils3 = require('../common/Utils'); + +var _Utils4 = _interopRequireDefault(_Utils3); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const SpecFileLoader = { + load(version, sdk_version) { + const APISpecDir = _path2.default.resolve(__dirname, '..', '..', 'api_specs'); + const overriddenAPISpecName = 'SDKCodegen'; + const overriddenAPISpecs = _Utils2.default.loadJSONFile(_path2.default.join(APISpecDir, overriddenAPISpecName + '.json')); + + // Compute version features + const versionedFeatures = {}; + const versionedFeaturesWithDepreciation = {}; + const codeGenFileDepreciationSign = _Utils4.default.codeGenFileDepreciationSign; + for (const currentVersion in _versions2.default) { + if (_Utils2.default.versionCompare(currentVersion, version) <= 0) { + if (_versions2.default[currentVersion]) { + const currentVersions = _versions2.default[currentVersion]; + currentVersions.forEach(feature => { + const hasFeatureName = 'has_' + feature; + versionedFeatures[hasFeatureName] = true; + versionedFeaturesWithDepreciation[hasFeatureName] = { + '@remove_file': codeGenFileDepreciationSign + }; + }); + } + } + } + + // Load API specs + const loadedAPISpecs = _Utils2.default.loadSpecsFromFile(APISpecDir); + + // merge versioned overridden API specs + return { + specs: loadedAPISpecs, + metadata: { + version: version, + sdk_version: sdk_version, + mergedOverriding: overriddenAPISpecs, + versionedFeatures: versionedFeatures, + versionedFeaturesWithDepreciation: versionedFeaturesWithDepreciation + } + }; + } +}; + +exports.default = SpecFileLoader; \ No newline at end of file diff --git a/lib/loaders/Utils.js b/lib/loaders/Utils.js new file mode 100644 index 00000000..8a8a718c --- /dev/null +++ b/lib/loaders/Utils.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const Utils = { + loadJSONFile(fileName, keepComments = false) { + let content = _fs2.default.readFileSync(fileName, 'utf8'); + if (!keepComments) { + content = content.replace(/^\/\/.*\n/gm, ''); + } + try { + return JSON.parse(content); + } catch (e) { + console.error('Failed ot parse json ', fileName); + throw e; + } + }, + + loadSpecsFromFile(specDir) { + // Load API specs + const specs = {}; + const enumMetadataMap = {}; + const versionedSpecDir = _path2.default.join(specDir, 'specs'); + _fs2.default.readdirSync(versionedSpecDir).forEach(file => { + const match = file.match(/^([a-z0-9_\-\.]+)\.json$/i); + if (match) { + const name = match[1]; + /** @type {Array<{ values: { [x: string]: any }; name: string; }>} */ + const json = this.loadJSONFile(_path2.default.join(versionedSpecDir, file)); + if ('enum_types' === name) { + json.forEach(enumType => { + for (const i in enumType.values) { + if (enumType.values[i].trim) { + enumType.values[i] = enumType.values[i].trim(); + } + } + + enumMetadataMap[enumType.name] = enumType; + }); + } else { + specs[name] = json; + } + } + }); + return { + api_specs: specs, + enumMetadataMap: enumMetadataMap + }; + }, + versionCompare(verA, verB) { + if (verA.charAt(0) != 'v' || verB.charAt(0) != 'v') { + throw new Error('invalid version number'); + } + const partsA = verA.substring(1).split('.'); + const partsB = verB.substring(1).split('.'); + for (let i = 0; i < Math.max(partsA.length, partsB.length); ++i) { + const numA = parseInt(partsA[i] || '-1'); + const numB = parseInt(partsB[i] || '-1'); + if (numA > numB) { + return 1; + } else if (numA < numB) { + return -1; + } + } + return 0; + } +}; + +exports.default = Utils; \ No newline at end of file diff --git a/lib/processors/AutoAddIdFieldForRootNodeProcessor.js b/lib/processors/AutoAddIdFieldForRootNodeProcessor.js new file mode 100644 index 00000000..15156873 --- /dev/null +++ b/lib/processors/AutoAddIdFieldForRootNodeProcessor.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + + +/* + * This processor add id field to fields if the class contains apis, which + * means it is a root node. This processor need to run earlier in the pipeline + */ +const AutoAddIdFieldForRootNodeProcessor = { + process(specs) { + const APISpecs = specs.api_specs; + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + let hasIdField = false; + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + if (fieldSpec.name === 'id') { + hasIdField = true; + break; + } + } + if (!hasIdField && APIClsSpec.apis && APIClsSpec.apis.length > 0) { + APIClsSpec.fields.push({ + name: 'id', + type: 'string' + }); + } + } + + return specs; + } +}; + +exports.default = AutoAddIdFieldForRootNodeProcessor; \ No newline at end of file diff --git a/lib/processors/CodeGenLanguageAdScripts.js b/lib/processors/CodeGenLanguageAdScripts.js new file mode 100644 index 00000000..b511570a --- /dev/null +++ b/lib/processors/CodeGenLanguageAdScripts.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * @format + * + */ + +'use strict'; + +// Any node whose name contains any of these strings will be omitted from the +// generated SDK code. Any edge whose return type's name contains any of these +// strings will also be omitted. + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const blacklistedNodes = ['CustomAudience', 'OfflineConversionDataSet']; + +// Any edge with an endpoint exactly matching any of these strings will be +// omitted from the generated SDK code. +const blacklistedEdges = ['/audiencereplace', '/batchreplace', '/batchupload', '/partnerdata', '/partnerrequests', '/usermatch', '/usersofanyaudience']; + +const isBlacklistedNode = name => blacklistedNodes.some(node => name.includes(node)); + +const isBlacklistedEdge = name => blacklistedEdges.some(edge => name == edge); + +const CodeGenLanguageNodeJs = { + formatFileName(clsName) { + return clsName['name:hyphen'] + '.js'; + }, + + preMustacheProcess(APISpecs, codeGenNameConventions, enumMetadataMap) { + for (const clsName in APISpecs) { + if (isBlacklistedNode(clsName)) { + delete APISpecs[clsName]; + continue; + } + + const APIClsSpec = APISpecs[clsName]; + if (APIClsSpec.apis !== undefined && APIClsSpec.apis !== null) { + APIClsSpec.apis = APIClsSpec.apis.filter(apiSpec => { + return (!apiSpec.return || !isBlacklistedNode(apiSpec.return)) && !isBlacklistedEdge(apiSpec.endpoint); + }); + } + + for (const _index in APIClsSpec.fields) { + const enumList = {}; + const newEnumSpecList = []; + for (const index2 in APIClsSpec.api_spec_based_enum_reference) { + const enumSpec = APIClsSpec.api_spec_based_enum_reference[index2]; + if (enumSpec['field_or_param:all_lower_case'] != 'fields') { + enumList[enumSpec.name] = enumSpec['field_or_param:all_lower_case']; + newEnumSpecList.push(enumSpec); + } + } + APIClsSpec.api_spec_based_enum_reference = newEnumSpecList; + } + } + return APISpecs; + }, + + keywords: ['try', 'private', 'public', 'new', 'default', 'class'] +}; + +exports.default = CodeGenLanguageNodeJs; \ No newline at end of file diff --git a/lib/processors/CodeGenLanguageCSharp.js b/lib/processors/CodeGenLanguageCSharp.js new file mode 100644 index 00000000..4ee7d519 --- /dev/null +++ b/lib/processors/CodeGenLanguageCSharp.js @@ -0,0 +1,259 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const getBaseType = type => type.replace(/(^|^list\s*<\s*)([a-zA-Z0-9_\s]*)($|\s*>\s*$)/i, '$2'); + +const getTypeForCSharp = type => { + if (!type) { + return null; + } + + // This is not perfect. But it's working for all types we have so far. + const typeMapping = { + string: /string|datetime/gi, + bool: /bool(ean)?/gi, + long: /(((unsigned\s*)?(int|long)))(?![a-zA-Z0-9_])/gi, + double: /(float|double)/gi, + 'List<$1>': /list\s*<\s*([a-zA-Z0-9_]*)\s*>/gi, + 'Dictionary<$1, $2>': /map\s*<\s*([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_]*)\s*>/gi, + '$1Dictionary$2': /(^|<)map($|>)/i + }; + + let oldType; + let newType = type; + while (oldType !== newType) { + oldType = newType; + for (const replace in typeMapping) { + newType = newType.replace(typeMapping[replace], replace); + } + } + newType = newType.replace(/list/g, 'List'); + newType = newType.replace(/^file$/i, 'FileInfo'); + return newType; +}; + +const CodeGenLanguageCSharp = { + formatFileName(clsName) { + return clsName['name:pascal_case'] + '.cs'; + }, + + preMustacheProcess(APISpecs, enumTypes, codeGenNameConventions) { + // Process APISpecs for CSharp + // 1. type normalization + // 2. enum type naming and referencing + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + const clsReferencedEnumTypes = {}; + const nameConflictCheck = { + /** + * @param {string | number} enumName + */ + hasConflict(enumName) { + if (this[enumName] == undefined) { + throw Error('Unexpected enum name!'); + } + return this[enumName] >= 2; + }, + /** + * @param {string | number} enumName + */ + add(enumName) { + this[enumName] = this[enumName] || 0; + this[enumName]++; + } + }; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + for (const index2 in APISpec.params) { + const paramSpec = APISpec.params[index2]; + if (['file', 'bytes', 'zipbytes'].indexOf(paramSpec.name) != -1) { + APISpec.params[index2] = undefined; + APISpec.allow_file_upload = true; + continue; + } + if (paramSpec.type) { + const baseType = getBaseType(paramSpec.type); + const enumType = {}; + if (enumTypes[baseType]) { + enumType.type = baseType; + enumType.name = paramSpec.name; + codeGenNameConventions.populateNameConventions(enumType, 'name', codeGenNameConventions.parseUnderscoreName(enumType.name)); + enumType['type:csharp:short'] = 'Enum' + enumType['name:pascal_case']; + enumType['type:csharp:long'] = 'Enum' + (APISpec['return:pascal_case'] || APISpec['name:pascal_case'].replace(/^create(.*?)$/i, '$1')) + enumType['name:pascal_case']; + enumType.obj = paramSpec; + enumType.api_spec = APISpec; + nameConflictCheck.add(enumType['type:csharp:short']); + nameConflictCheck.add(enumType['type:csharp:long']); + if (!clsReferencedEnumTypes[baseType]) { + clsReferencedEnumTypes[baseType] = [enumType]; + } else { + clsReferencedEnumTypes[baseType].push(enumType); + } + } else { + paramSpec['type:csharp'] = getTypeForCSharp(paramSpec.type); + if (paramSpec['type:csharp'] == 'string') { + paramSpec.is_string = true; + } + } + } + } + if (APISpec.params) { + APISpec.params = APISpec.params.filter(element => { + return element != null; + }); + } + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + const fieldCls = APISpecs[fieldSpec.type]; + if (fieldCls && fieldCls.has_get && fieldCls.has_id) { + fieldSpec.is_root_node = true; + } + if (fieldSpec.type) { + const baseType = getBaseType(fieldSpec.type); + // HACKHACK + if (APISpecs[baseType] || baseType === 'Campaign' || baseType === 'AdSet') { + fieldSpec.is_node = true; + fieldSpec['csharp:node_base_type'] = getTypeForCSharp(baseType); + } else { + const baseCSType = getTypeForCSharp(baseType); + fieldSpec['csharp:base_type'] = baseCSType; + if (baseCSType === 'string') { + fieldSpec.is_string_base_type = true; + } else if (baseCSType === 'long') { + fieldSpec.is_long_base_type = true; + } else if (baseCSType === 'double') { + fieldSpec.is_double_base_type = true; + } else if (baseCSType === 'bool') { + fieldSpec.is_bool_base_type = true; + } else if (baseCSType === 'object' || baseCSType === 'Object') { + fieldSpec.is_object_base_type = true; + } + } + if (/^list { + const enumType = clsReferencedEnumTypes[key][0]; + const baseType = enumType.type; + const enumValues = []; + for (const i in enumTypes[baseType]) { + const apiValue = enumTypes[baseType][i]; + let csharpName = apiValue; + if (apiValue.split) { + const parts = []; + apiValue.split(/[._:\s]/).map( + /** + * @param {{ toUpperCase: () => void; }} part + */ + part => { + parts.push(part.toUpperCase()); + }); + csharpName = parts.join('_'); + csharpName = csharpName.replace(/[^a-zA-Z0-9_]/g, ''); + } + // this is to handle situations where we allow both + // "IN_STOCK" and "in stock" (or similar) in api, + // but we only need to support one in SDK because they + // are the same. + let valueExists = false; + for (let i = 0; i < enumValues.length; i++) { + if (enumValues[i]['csharp_name'] === csharpName) { + valueExists = true; + break; + } + } + if (!valueExists) { + enumValues.push({ + api_value: apiValue, + csharp_name: csharpName + }); + } + } + enumType.values = enumValues; + + let realName; + let isApiEnum = false; + if (clsReferencedEnumTypes[key].length > 1) { + realName = enumType['type:csharp:short']; + } else if (!nameConflictCheck.hasConflict(enumType['type:csharp:short'])) { + realName = enumType['type:csharp:short']; + } else if (!nameConflictCheck.hasConflict(enumType['type:csharp:long'])) { + realName = enumType['type:csharp:long']; + } else if (!enumType.api_spec) { + realName = enumType['type:csharp:long']; + } else { + const apiSpec = enumType.api_spec; + if (!apiSpec.api_referenced_enum_types) { + apiSpec.api_referenced_enum_types = [enumType]; + } else { + apiSpec.api_referenced_enum_types.push(enumType); + } + realName = enumType['type:csharp:short']; + isApiEnum = true; + } + + enumType['type:csharp'] = realName; + clsReferencedEnumTypes[key].map( + /** + * @param {{ [x: string]: any; }} e + */ + e => { + const obj = e.obj; + obj.is_enum = true; + obj['type:csharp'] = getTypeForCSharp(obj.type.replace(baseType, enumType['type:csharp'])); + obj.is_enum = true; + delete e.obj; + }); + delete enumType['type:csharp:short']; + delete enumType['type:csharp:long']; + delete enumType.api_spec; + return isApiEnum ? null : enumType; + }); + APIClsSpec.cls_referenced_enum_types = APIClsSpec['cls_referenced_enum_types'].filter( + /** + * @param {null} element + */ + element => { + return element != null; + }); + } + return APISpecs; + }, + getTypeForCSharp: getTypeForCSharp, + keywords: ['try', 'private', 'public', 'new', 'default', 'class'] +}; + +exports.default = CodeGenLanguageCSharp; \ No newline at end of file diff --git a/lib/processors/CodeGenLanguageJava.js b/lib/processors/CodeGenLanguageJava.js new file mode 100644 index 00000000..de836b5e --- /dev/null +++ b/lib/processors/CodeGenLanguageJava.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const getBaseType = type => { + return type.replace(/(^|^list\s*<\s*)([a-zA-Z0-9_\s]*)($|\s*>\s*$)/i, '$2'); +}; + +const CodeGenLanguageJava = { + formatFileName(clsName) { + return clsName['name:pascal_case'] + '.java'; + }, + + specOverrideProcessing(APISpecs) { + // Handling fields that are gated by capabilities + // There fields will break requestAllFields in Java SDK + for (const className in APISpecs) { + const APIClsSpec = APISpecs[className]; + for (const fieldIndex in APIClsSpec.fields) { + let fieldSpec = APIClsSpec.fields[fieldIndex]; + fieldSpec.not_visible |= fieldSpec['java:not_visible']; + } + } + return APISpecs; + }, + + preMustacheProcess(APISpecs, codeGenNameConventions, enumMetadataMap) { + let javaBaseType; + + // Process APISpecs for Java + // 1. type normalization + // 2. enum type naming and referencing + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + for (const index2 in APISpec.params) { + const paramSpec = APISpec.params[index2]; + /* when we have a param called 'params', + * see GraphProductFeedRulesPost, + * We will have two SetParams functions in the api. One is + * to set all params for the api, which is of type + * Map. Another is to set the individual parameter + * named 'params'. If the type of the parameter is some kind of Map, + * it will cause "have the same erasure" error in Java because + * Java cannot distinguish different Map during runtime. + * So we add a flag here to indicate the 'params' param and in + * template, we generate it as SetParamParams to avoid conflict + */ + if (paramSpec.name == 'params') { + paramSpec.param_name_params = true; + } + if (['file', 'bytes', 'zipbytes'].indexOf(paramSpec.name) != -1) { + APISpec.params[index2] = undefined; + APISpec.allow_file_upload = true; + continue; + } + if (paramSpec.type) { + const baseType = getBaseType(paramSpec.type); + if (enumMetadataMap[baseType]) { + paramSpec.is_enum_param = true; + const metadata = enumMetadataMap[baseType]; + if (!metadata.node) { + if (!APIClsSpec.api_spec_based_enum_reference) { + APIClsSpec.api_spec_based_enum_reference = []; + APIClsSpec.api_spec_based_enum_list = {}; + } + if (!APIClsSpec.api_spec_based_enum_list[metadata.field_or_param]) { + APIClsSpec.api_spec_based_enum_reference.push(metadata); + APIClsSpec.api_spec_based_enum_list[metadata.field_or_param] = true; + } + javaBaseType = 'Enum' + metadata['field_or_param:pascal_case']; + } else { + javaBaseType = metadata.node + '.Enum' + metadata['field_or_param:pascal_case']; + } + paramSpec['type:java'] = this.getTypeForJava(paramSpec.type.replace(baseType, javaBaseType)); + paramSpec['basetype:java'] = javaBaseType; + } else { + paramSpec['type:java'] = this.getTypeForJava(paramSpec.type); + if (paramSpec['type:java'] == 'String') { + paramSpec.is_string = true; + } + } + } + } + if (APISpec.params) { + APISpec.params = APISpec.params.filter(element => element != null); + } + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + const fieldCls = APISpecs[fieldSpec.type]; + if (fieldCls && fieldCls.has_get && fieldCls.has_id) { + fieldSpec.is_root_node = true; + } + if (fieldSpec.type) { + if (enumMetadataMap[fieldSpec.type]) { + fieldSpec.is_enum_field = true; + } + const baseType = getBaseType(fieldSpec.type); + if (APISpecs[baseType]) { + fieldSpec.is_node = true; + fieldSpec['java:node_base_type'] = this.getTypeForJava(baseType); + } + if (enumMetadataMap[baseType]) { + const metadata = enumMetadataMap[baseType]; + javaBaseType = 'Enum' + metadata['field_or_param:pascal_case']; + fieldSpec['type:java'] = this.getTypeForJava(fieldSpec.type.replace(baseType, javaBaseType)); + if (!APIClsSpec.api_spec_based_enum_reference) { + APIClsSpec.api_spec_based_enum_reference = []; + APIClsSpec.api_spec_based_enum_list = {}; + } + if (!APIClsSpec.api_spec_based_enum_list[metadata.field_or_param]) { + APIClsSpec.api_spec_based_enum_reference.push(metadata); + APIClsSpec.api_spec_based_enum_list[metadata.field_or_param] = true; + } + } else { + if (fieldSpec.keyvalue) { + fieldSpec['type:java'] = 'List'; + } else if (fieldSpec.targeting_optimization_types) { + fieldSpec['type:java'] = 'List'; + } else { + fieldSpec['type:java'] = this.getTypeForJava(fieldSpec.type); + } + } + } + } + } + return APISpecs; + }, + + getTypeForJava(type) { + if (!type) { + return null; + } + + // This is not perfect. But it's working for all types we have so far. + const typeMapping = { + String: /string|datetime/gi, + Boolean: /bool(ean)?/gi, + Long: /(((unsigned\s*)?(\bint|long)))(?![a-zA-Z0-9_])/gi, + Double: /(float|double)/gi, + 'List<$1>': /list\s*<\s*([a-zA-Z0-9_.<>,\s]*?)\s*>/g, + '$1Map$2': /(^|<)map($|>)/i, + 'Map<$1, $2>': /map\s*<\s*([a-zA-Z0-9_]*?)\s*,\s*([a-zA-Z0-9_<>, ]*?)\s*>/g + }; + + let oldType; + let newType = type; + while (oldType !== newType) { + oldType = newType; + for (const replace in typeMapping) { + newType = newType.replace(typeMapping[replace], replace); + } + } + // This is to make a type named as list to JsonArray. + // However the 'list' should not be preceden by any word, + // which might be 'blacklist', and should not be replaced + newType = newType.replace(/(? => List + newType = newType.replace(//i, ''); + return newType; + }, + keywords: ['try', 'private', 'public', 'new', 'default', 'class'] +}; + +exports.default = CodeGenLanguageJava; diff --git a/lib/processors/CodeGenLanguageNodeJs.js b/lib/processors/CodeGenLanguageNodeJs.js new file mode 100644 index 00000000..61aeaf50 --- /dev/null +++ b/lib/processors/CodeGenLanguageNodeJs.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const CodeGenLanguageNodeJs = { + formatFileName(clsName) { + return clsName['name:hyphen'] + '.js'; + }, + + preMustacheProcess(APISpecs, codeGenNameConventions, enumMetadataMap) { + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + let containsGenericReferece = false; + let containsReadEdges = false; + for (const index in APIClsSpec.apis) { + const apiSpec = APIClsSpec.apis[index]; + if (!apiSpec.return) { + containsGenericReferece = true; + } + if (apiSpec.method == 'GET' && apiSpec.endpoint != '/') { + containsReadEdges = true; + } + } + if (containsGenericReferece) { + APIClsSpec.has_generic_reference = true; + } + if (containsReadEdges) { + APIClsSpec.has_read_edges = true; + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + + const enumList = {}; + const newEnumSpecList = []; + for (const index2 in APIClsSpec.api_spec_based_enum_reference) { + const enumSpec = APIClsSpec.api_spec_based_enum_reference[index2]; + if (enumSpec['field_or_param:all_lower_case'] != 'fields') { + enumList[enumSpec.name] = enumSpec['field_or_param:all_lower_case']; + newEnumSpecList.push(enumSpec); + } + } + APIClsSpec.api_spec_based_enum_reference = newEnumSpecList; + } + } + return APISpecs; + }, + + keywords: ['try', 'private', 'public', 'new', 'default', 'class', 'do'] +}; + +exports.default = CodeGenLanguageNodeJs; \ No newline at end of file diff --git a/lib/processors/CodeGenLanguageRuby.js b/lib/processors/CodeGenLanguageRuby.js new file mode 100644 index 00000000..317c6443 --- /dev/null +++ b/lib/processors/CodeGenLanguageRuby.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const typeStackToHash = typeStack => { + const t = typeStack.shift(); + + switch (t) { + case 'list': + if (typeStack.length > 0) { + return '{ list: ' + typeStackToHash(typeStack) + ' }'; + } else { + // handle list, seems only AdImageCrop use this + // we assume it's string + return "{ list: 'string' }"; + } + case 'enum': + const enum_values = typeStack.shift(); + if (enum_values instanceof Array) { + return '{ enum: %w{' + enum_values.join(' ') + ' }}'; + } else { + return '{ enum: -> { ' + enum_values + ' }}'; + } + default: + return "'" + t + "'"; + } +}; + +const getTypeForRuby = (type, enumList, references) => { + const typeStack = []; + const listRegex = /^list(?:<(.*)>)?$/i; + + while (listRegex.test(type)) { + typeStack.push('list'); + type = type.replace(listRegex, '$1'); + } + + // This is not perfect. But it's working for all types we have so far. + const typeMapping = { + string: /^string$/i, + datetime: /^datetime$/i, + bool: /^bool(ean)?$/i, + int: /^(((unsigned\s*)?(int|long)))(?![a-zA-Z0-9_])$/i, + double: /^(float|double)$/i, + object: /^Object$/i, + hash: /^map(?:\s*<\s*(?:[a-zA-Z0-9_]*)\s*,\s*(?:[a-zA-Z0-9_]*)\s*>)?$/i + }; + + for (const replace in typeMapping) { + if (typeMapping[replace].test(type)) { + typeStack.push(replace); + type = type.replace(typeMapping[replace], ''); + } + } + + if (enumList[type]) { + typeStack.push('enum'); + typeStack.push(enumList[type]); + type = ''; + } + + //non native, non enum type - should be other AdObject subclass + if (type.length > 0) { + if (references[type]) { + typeStack.push(references[type]['names:strict_pascal_case']); + } else { + typeStack.push(type); + } + } + + return typeStackToHash(typeStack); +}; + +const CodeGenLanguageRuby = { + formatFileName(clsName) { + return clsName['name:underscore'] + '.rb'; + }, + + preMustacheProcess(APISpecs) { + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + + // Fields + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + + const enumList = {}; + for (const index2 in APIClsSpec.api_spec_based_enum_reference) { + const enumSpec = APIClsSpec.api_spec_based_enum_reference[index2]; + enumList[enumSpec.name] = enumSpec['field_or_param:upper_case']; + } + + const rubyType = getTypeForRuby(fieldSpec.type, enumList, APISpecs); + + fieldSpec['type:ruby'] = rubyType; + } + + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + + const enumList = {}; + for (const index2 in APISpec.referred_enums) { + const enumSpec = APISpec.referred_enums[index2]; + if (enumSpec.metadata.node) { + let node_name = enumSpec.metadata.node; + if (APISpecs[node_name]) { + node_name = APISpecs[node_name]['name:strict_pascal_case']; + } + enumList[enumSpec.metadata.name] = node_name + '::' + enumSpec.metadata['field_or_param:upper_case']; + } else { + enumList[enumSpec.metadata.name] = enumSpec.metadata.values; + } + } + + for (const index2 in APISpec.params) { + const paramSpec = APISpec.params[index2]; + const rubyType = getTypeForRuby(paramSpec.type, enumList, APISpecs); + paramSpec['type:ruby'] = rubyType; + } + + if (APISpec.name == 'update' && APISpec.endpoint == '/') { + APIClsSpec.has_update = true; + } + + if (APISpec.name == 'delete' && APISpec.endpoint == '/') { + APIClsSpec.has_delete = true; + } + } + + // Restructure Edges arrays + let edgeArray = APIClsSpec.edges; + const edgeObject = {}; + + for (const index in APIClsSpec.edges) { + const edgeSpec = APIClsSpec.edges[index]; + const edgeName = edgeSpec.endpoint.replace(/^\//, ''); + + if (!edgeObject[edgeName]) { + edgeObject[edgeName] = []; + } + edgeSpec['method:lower_case'] = edgeSpec.method.toLowerCase(); + + if (edgeSpec.name == 'create_ad_image') { + edgeSpec.return_a_list = true; + } + + edgeObject[edgeName].push(edgeSpec); + } + + edgeArray = []; + for (const edgeName in edgeObject) { + const edgeEndPoints = edgeObject[edgeName]; + edgeArray.push({ + edge_name: edgeName, + end_points: edgeEndPoints + }); + } + + APIClsSpec.edges = edgeArray; + } + + return APISpecs; + }, + getTypeForRuby: getTypeForRuby, + keywords: ['class', 'begin', 'end', 'rescue', 'when', 'case', 'def', 'until', 'do'] +}; + +exports.default = CodeGenLanguageRuby; \ No newline at end of file diff --git a/lib/processors/CodeGenLanguages.js b/lib/processors/CodeGenLanguages.js new file mode 100644 index 00000000..66d1b7f8 --- /dev/null +++ b/lib/processors/CodeGenLanguages.js @@ -0,0 +1,150 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mustache = require('mustache'); + +var _CodeGenLanguageJava = require('./CodeGenLanguageJava'); + +var _CodeGenLanguageJava2 = _interopRequireDefault(_CodeGenLanguageJava); + +var _CodeGenLanguageRuby = require('./CodeGenLanguageRuby'); + +var _CodeGenLanguageRuby2 = _interopRequireDefault(_CodeGenLanguageRuby); + +var _CodeGenLanguageNodeJs = require('./CodeGenLanguageNodeJs'); + +var _CodeGenLanguageNodeJs2 = _interopRequireDefault(_CodeGenLanguageNodeJs); + +var _CodeGenLanguageAdScripts = require('./CodeGenLanguageAdScripts'); + +var _CodeGenLanguageAdScripts2 = _interopRequireDefault(_CodeGenLanguageAdScripts); + +var _CodeGenUtil = require('./CodeGenUtil'); + +var _CodeGenUtil2 = _interopRequireDefault(_CodeGenUtil); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const generateFieldEnumReferences = (APISpecs, enumMetadataMap) => { + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + const fieldSpecs = APIClsSpec.fields; + for (const fieldIndex in fieldSpecs) { + const fieldSpec = fieldSpecs[fieldIndex]; + const baseType = _CodeGenUtil2.default.getBaseType(fieldSpec.type); + if (enumMetadataMap[baseType]) { + const type = enumMetadataMap[baseType]['field_or_param:pascal_case']; + fieldSpec['type:short'] = fieldSpec.type.replace(baseType, type); + continue; + } else { + fieldSpec['type:short'] = fieldSpec.type; + } + } + } +// @fb-only + + +const CodeGenLanguages = { + // @fb-only + nodejs: _CodeGenLanguageNodeJs2.default, + php: { + formatFileName(clsName, template) { + if (template.dir === '/src/FacebookAds/Object/Fields/') { + return clsName['name:pascal_case'] + 'Fields.php'; + } + return clsName['name:pascal_case'] + '.php'; + }, + preMustacheProcess(APISpecs, codeGenNameConventions, enumMetadataMap) { + generateFieldEnumReferences(APISpecs, enumMetadataMap); + for (const clsName in APISpecs) { + const allEnumRefs = {}; + const ClsSpec = APISpecs[clsName]; + for (const index in ClsSpec.api_spec_based_enum_reference) { + const enumMetadata = ClsSpec.api_spec_based_enum_reference[index]; + if (!enumMetadata.node) { + continue; + } + const enumPHPClassName = enumMetadata['node:pascal_case'] + enumMetadata['field_or_param:pascal_case'] + 'Values'; + allEnumRefs[enumPHPClassName] = true; + } + for (const apiName in ClsSpec.apis) { + const APISpec = ClsSpec.apis[apiName]; + for (const index in APISpec.referred_enums) { + const enumMetadata = APISpec.referred_enums[index]['metadata']; + if (!enumMetadata.node) { + continue; + } + const enumPHPClassName = enumMetadata['node:pascal_case'] + enumMetadata['field_or_param:pascal_case'] + 'Values'; + allEnumRefs[enumPHPClassName] = true; + } + } + ClsSpec['php:all_referred_enum_names'] = []; + for (const enumName in allEnumRefs) { + ClsSpec['php:all_referred_enum_names'].push(enumName); + } + ClsSpec['php:all_referred_enum_names'].sort(); + } + return APISpecs; + }, + + generateFilenameToCodeMap(clsSpec, template, partialTemplates) { + const filenameToCodeMap = {}; + if (template.dir === '/src/FacebookAds/Object/Values/') { + if (!('api_spec_based_enum_reference' in clsSpec)) { + return {}; + } + const enumReferences = clsSpec.api_spec_based_enum_reference; + enumReferences.forEach(enumReference => { + const filename = enumReference['node:pascal_case'] + enumReference['field_or_param:pascal_case'] + 'Values.php'; + + const code = (0, _mustache.render)(template.content, enumReference, partialTemplates); + if (code && code.length > 0) { + filenameToCodeMap[filename] = code; + } + }); + } else { + const filename = this.formatFileName(clsSpec, template); + + const code = (0, _mustache.render)(template.content, clsSpec, partialTemplates); + if (code && code.length > 0) { + filenameToCodeMap[filename] = code; + } + } + + return filenameToCodeMap; + }, + keywords: ['try', 'private', 'public', 'new', 'default', 'class', 'global', 'as', 'do', 'empty'] + }, + python: { + formatFileName(clsName) { + return clsName['name:all_lower_case'] + '.py'; + }, + preMustacheProcess(APISpecs, codeGenNameConventions, enumMetadataMap) { + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + fieldSpec.index = index; + } + } + + generateFieldEnumReferences(APISpecs, enumMetadataMap); + return APISpecs; + }, + keywords: ['try', 'default', 'class', 'global', 'in', 'from', 'with', 'as', 'is'] + }, + java: _CodeGenLanguageJava2.default, + ruby: _CodeGenLanguageRuby2.default +}; + +exports.default = CodeGenLanguages; \ No newline at end of file diff --git a/lib/processors/CodeGenNameConventions.js b/lib/processors/CodeGenNameConventions.js new file mode 100644 index 00000000..0c0f3f88 --- /dev/null +++ b/lib/processors/CodeGenNameConventions.js @@ -0,0 +1,337 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _merge = require('merge'); + +var _merge2 = _interopRequireDefault(_merge); + +var _path = require('path'); + +var _pluralize = require('pluralize'); + +var _pluralize2 = _interopRequireDefault(_pluralize); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const _startsWith = (str, searchStr, startIdx) => { + if (str.length < startIdx + searchStr.length) { + return false; + } + + let index = 0; + while (index < searchStr.length) { + if (str.charAt(index + startIdx) !== searchStr.charAt(index)) { + return false; + } + ++index; + } + return true; +}; + +const _isCharUpper = ch => { + return ch >= 'A' && ch <= 'Z'; +}; + +const _decodeDictFile = (0, _path.resolve)(__dirname, '..', '..', 'api_specs', 'EndpointDecodeDict.txt'); +let _decodeDictBase = {}; +if ((0, _fs.existsSync)(_decodeDictFile)) { + _decodeDictBase = (0, _fs.readFileSync)(_decodeDictFile, 'utf8').split('\n').reduce((dict, line) => { + const columns = line.split('='); + const value = parseFloat(columns[1]); + if (value) { + dict[columns[0].trim()] = value; + } + return dict; + }, {}); +} + +let _decodeDict = _decodeDictBase; +const CodeGenNameConventions = { + initDecodeDictionary(words) { + const counter = {}; + words.forEach(val => { + const w = val.toLowerCase(); + counter[w] = (counter[w] || 0) + 1; + const pw = (0, _pluralize2.default)(w); + if (pw !== w) { + counter[pw] = (counter[pw] || 0) + 0.5; + } + }); + + let maxCnt = 1; + for (const w in counter) { + if (counter[w] > maxCnt) { + maxCnt = counter[w]; + } + } + + _decodeDict = (0, _merge2.default)(true, _decodeDictBase); + for (const w in counter) { + _decodeDict[w] = counter[w] / maxCnt; + } + }, + + parseEndpointName(endpoint, boostWords, clsName) { + if (endpoint === 'leadgen_forms') { + return ['lead', 'gen', 'forms']; + } + if (endpoint === 'leadgen_context_cards') { + return ['lead', 'gen', 'context', 'cards']; + } + if (endpoint === 'leadgen_whitelisted_users') { + return ['lead', 'gen', 'whitelisted', 'users']; + } + if (endpoint === 'ExtendedCreditOwningCreditAllocationConfigs') { + return ['extended', 'credit', 'owning', 'credit', 'allocation', 'configs']; + } + + let mergedDict = _decodeDict; + if (boostWords) { + mergedDict = (0, _merge2.default)(true, _decodeDict, boostWords.reduce((prev, curr) => { + prev[curr] = 10; + const pluralWord = (0, _pluralize2.default)(curr); + if (pluralWord !== curr) { + prev[pluralWord] = 1; + } + return prev; + }, {})); + } + + // $FlowFixMe + const lattice = { 0: [0] }; + const candidates = [1]; + for (let index = 0; index < endpoint.length; ++index) { + if (!candidates[index]) { + continue; + } + + const weight = lattice[index][0]; + let newWeight = 0; + // deal with the underscores in endpoints + if (endpoint.charAt(index) === '_') { + const newIndex = index + 1; + if (!lattice[newIndex]) { + lattice[newIndex] = [newWeight, '#']; + candidates[newIndex] = 1; + } + } + + for (const word in mergedDict) { + if (_startsWith(endpoint, word, index)) { + const newIndex = index + word.length; + newWeight = weight + mergedDict[word]; + if (!lattice[newIndex] || lattice[newIndex][0] < newWeight) { + lattice[newIndex] = [newWeight, word]; + candidates[newIndex] = 1; + } + } + } + } + + const parts = []; + let endPost = endpoint.length; + if (!lattice[endPost]) { + throw Error('cannot decode endpoint ' + endpoint + ' in class ' + clsName); + } + while (endPost) { + if (lattice[endPost][1] !== '#') { + parts.push(lattice[endPost][1]); + } + endPost -= lattice[endPost][1].length; + } + + parts.reverse(); + return parts; + }, + + parseUnderscoreName(name) { + if (!name) { + return []; + } + if (!name.split) { + return [String(name)]; + } + return name.split(/[._:\s]/).map(part => part.toLowerCase()); + }, + + escapeSingleQuotes(input) { + if (input == null) + return input; + return input.replace(/'/g, "\\'"); + }, + + // The parsing of pascal name has a weird pitfall + // If the pased name is normal pascal style, the returned parts will be all + // lower case. If any part of the pascal name has all upper case, the return + // part will keep all upper case. Which can cause confusion for later usage + // + // The one big issue is for ruby. Ruby assume the name are all capitalized + // when autoload classes. Will just fix it for ruby case and keep this + // behavior this time. Will fix after f8. + parsePascalName(name) { + if (!name) { + return []; + } + if (!name.split) { + return [String(name)]; + } + if (name.charAt(0) !== name.charAt(0).toUpperCase()) { + throw Error('not valid pascal casing name: ' + name); + } + const STATUS = { + SEEN_WORD_START: 0, // seen an upper-case char after lower-case chars + EXPECT_PASCAL_WORD: 1, // second char is lower-case in a word + EXPECT_ALL_UPPER_WORD: 2 // second char is also upper case in a word + }; + const parts = []; + let indexStart = 0; + let parseStatus = STATUS.SEEN_WORD_START; // assert charAt(0) is upper case + for (let i = 1; i < name.length; ++i) { + const isUpper = _isCharUpper(name.charAt(i)); + switch (parseStatus) { + case STATUS.SEEN_WORD_START: + parseStatus = isUpper ? STATUS.EXPECT_ALL_UPPER_WORD : STATUS.EXPECT_PASCAL_WORD; + break; + case STATUS.EXPECT_PASCAL_WORD: + if (isUpper) { + // The word terminates when we see the next upper-case letter + parts.push(name.substring(indexStart, i).toLowerCase()); + indexStart = i; + parseStatus = STATUS.SEEN_WORD_START; + } + break; + case STATUS.EXPECT_ALL_UPPER_WORD: + if (!isUpper) { + // The word terminates when we see a lower-case letter + parts.push(name.substring(indexStart, i - 1)); + indexStart = i - 1; + parseStatus = STATUS.EXPECT_PASCAL_WORD; + } + break; + } + } + + const lastPart = name.substring(indexStart, name.length); + if (parseStatus === STATUS.EXPECT_ALL_UPPER_WORD) { + parts.push(lastPart); + } else { + parts.push(lastPart.toLowerCase()); + } + return parts; + }, + + /** + * Replace non (alphanumerical + _) to _ + */ + removeIlligalChars(name) { + return name.replace(/(_\W+)|(\W+_)|(\W+)/g, '_'); + }, + + // strict_pascal means strictly first char upper and following lower. + // See comments in parsePascalName + populateNameConventions(obj, prop, parts, prefix) { + if (!parts || parts.length == 0) { + return; + } + const lowerCaseParts = []; + const capitalized = []; + const strictCapitalized = []; + + parts.forEach(val => { + lowerCaseParts.push(val.toLowerCase()); + capitalized.push(val.charAt(0).toUpperCase() + val.slice(1)); + strictCapitalized.push(val.charAt(0).toUpperCase() + val.slice(1).toLowerCase()); + }); + + const hyphenName = lowerCaseParts.join('-'); + const underscoreName = lowerCaseParts.join('_'); + const upperCaseName = underscoreName.toUpperCase(); + let camelCaseName = lowerCaseParts[0]; + let pascalCaseName = capitalized[0]; + let strictPascalCaseName = strictCapitalized[0]; + let allLowerCaseName = lowerCaseParts[0]; + for (let i = 1; i < parts.length; ++i) { + camelCaseName += capitalized[i]; + pascalCaseName += capitalized[i]; + strictPascalCaseName += strictCapitalized[i]; + allLowerCaseName += lowerCaseParts[i]; + } + + obj[prop + ':hyphen'] = hyphenName; + obj[prop + ':underscore'] = underscoreName; + obj[prop + ':pascal_case'] = pascalCaseName; + obj[prop + ':strict_pascal_case'] = strictPascalCaseName; + obj[prop + ':camel_case'] = camelCaseName; + obj[prop + ':upper_case'] = upperCaseName; + obj[prop + ':all_lower_case'] = allLowerCaseName; + obj[prop + ':all_lower_case_excluding_digit_suffix'] = isNaN(Number(underscoreName.charAt(0))) ? underscoreName : 'value_' + underscoreName; + obj[prop + ':escape_single_quote'] = this.escapeSingleQuotes(obj[prop]); + + if (prefix) { + obj[prefix + prop + ':hyphen'] = hyphenName; + obj[prefix + prop + ':underscore'] = underscoreName; + obj[prefix + prop + ':pascal_case'] = pascalCaseName; + obj[prefix + prop + ':strict_pascal_case'] = strictPascalCaseName; + obj[prefix + prop + ':camel_case'] = camelCaseName; + obj[prefix + prop + ':upper_case'] = upperCaseName; + obj[prefix + prop + ':all_lower_case'] = allLowerCaseName; + obj[prefix + prop + ':all_lower_case_excluding_digit_suffix'] = isNaN(Number(allLowerCaseName.charAt(0))) ? allLowerCaseName : 'value_' + allLowerCaseName; + obj[prefix + prop + ':escape_single_quote'] = this.escapeSingleQuotes(obj[prop]); + } + }, + + getAllCaseNames(parts) { + const lowerCaseParts = []; + const capitalized = []; + + parts.forEach(val => { + lowerCaseParts.push(val.toLowerCase()); + capitalized.push(val.charAt(0).toUpperCase() + val.slice(1)); + }); + + const hyphenName = lowerCaseParts.join('-'); + const underscoreName = lowerCaseParts.join('_'); + const upperCaseName = underscoreName.toUpperCase(); + let camelCaseName = lowerCaseParts[0]; + let pascalCaseName = capitalized[0]; + let allLowerCaseName = lowerCaseParts[0]; + for (let i = 1; i < parts.length; ++i) { + camelCaseName += capitalized[i]; + pascalCaseName += capitalized[i]; + allLowerCaseName += lowerCaseParts[i]; + } + + return { + hyphen: hyphenName, + underscore: underscoreName, + pascal_case: pascalCaseName, + camel_case: camelCaseName, + upper_case: upperCaseName, + all_lower_case: allLowerCaseName + }; + }, + + populateNameConventionForUnderscoreProp(obj, prop) { + const parts = this.parseUnderscoreName(obj[prop]); + this.populateNameConventions(obj, prop, parts); + }, + + populateNameConventionForPascalProp(obj, prop) { + const parts = this.parsePascalName(obj[prop]); + this.populateNameConventions(obj, prop, parts); + } +}; + +exports.default = CodeGenNameConventions; diff --git a/lib/processors/CodeGenUtil.js b/lib/processors/CodeGenUtil.js new file mode 100644 index 00000000..a073077a --- /dev/null +++ b/lib/processors/CodeGenUtil.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const CodeGenUtil = { + getBaseType(type) { + return type.replace(/(^|^list\s*<\s*)([a-zA-Z0-9_\s]*)($|\s*>\s*$)/i, '$2'); + }, + + preProcessMainTemplates(mainTemplates, clsSpec) { + return mainTemplates.map(template => { + const newTemplate = JSON.parse(JSON.stringify(template)); + newTemplate.content = newTemplate.content.replace(/{{\s*>.*(%([a-zA-Z:_]+)%).*}}/gi, (m, p1, p2) => m.replace(p1, clsSpec[p2])); + return newTemplate; + }); + }, + + versionCompare(verA, verB) { + if (verA.charAt(0) != 'v' || verB.charAt(0) != 'v') { + throw new Error('invalid version number'); + } + const partsA = verA.substring(1).split('.'); + const partsB = verB.substring(1).split('.'); + for (let i = 0; i < Math.max(partsA.length, partsB.length); ++i) { + const numA = parseInt(partsA[i] || '-1'); + const numB = parseInt(partsB[i] || '-1'); + if (numA > numB) { + return 1; + } else if (numA < numB) { + return -1; + } + } + return 0; + } +}; + +exports.default = CodeGenUtil; \ No newline at end of file diff --git a/lib/processors/CreationEndpointHackProcessor.js b/lib/processors/CreationEndpointHackProcessor.js new file mode 100644 index 00000000..14fb9723 --- /dev/null +++ b/lib/processors/CreationEndpointHackProcessor.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +/* + * This processor is a hack for so-called 'creation endpoints'. + * This is mainly for backward compatibility and we're deprecating the support + * of it. + * ===== Background ===== + * The creation endpoints is used in legacy Python and PHP SDK which + * allows user to do something like: + * new AdCreative(parent_id)->setData(xxxx)->create() + * When this happens, SDK automatically resolve it by knowing parent_id to be + * an AdAccount ID and calls AdAccount/adcreatives (the creation endpoint) for + * creation. + * However, we prefer people to call AdAccount->createAdCreative() directly to + * explicitly indicate which creation endpoint is desired, because in some + * situations there could be multiple creation endpoints and the old-fashioned + * creation syntax can be ambiguous. + * ===== End of Background ===== + * + * This processor tries to figure out the creation endpoints of nodes by + * checking the return type of POST calls of certain nodes (clsWithCreationApi). + * It will throw error if there are multiple creation endpoints for a node, + * unless the preferred one is specified in codegen/api_specs/SDKCodegen.json + */ + +Object.defineProperty(exports, "__esModule", { + value: true +}); + + +const processor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + // Handling object creation endpoints + const clsWithCreationApi = ['AdAccount', 'Business', 'ProductCatalog', 'Hotel']; + for (let clsIndex = 0; clsIndex < clsWithCreationApi.length; clsIndex++) { + const parentClsName = clsWithCreationApi[clsIndex]; + const parentClsSpec = APISpecs[parentClsName]; + if (!parentClsSpec) { + continue; + } + for (const index in parentClsSpec.apis) { + const APISpec = parentClsSpec.apis[index]; + // We check for POST method with return type + if (APISpec.method === 'POST') { + const createdCls = APISpec.return; + if (createdCls && createdCls !== parentClsName) { + const createdClsSpec = APISpecs[createdCls]; + const creationEndpoint = APISpec.endpoint; + if (createdClsSpec && !createdClsSpec.exclude_creation_endpoint && creationEndpoint) { + if (createdClsSpec.creation_endpoint) { + createdClsSpec.multi_creation_endpoints = true; + } + + // we found multiple creation enpoints. only the one marked as + // 'preferred_creation_endpoint' will be taken. Otherwise, throw + if (!createdClsSpec.multi_creation_endpoints || APISpec.preferred_creation_endpoint) { + createdClsSpec.creation_parent_class = parentClsSpec.name; + createdClsSpec.creation_endpoint = creationEndpoint; + createdClsSpec.creation_method = APISpec.name; + if (APISpec.preferred_creation_endpoint) { + createdClsSpec.preferred_creation_endpoint = APISpec.preferred_creation_endpoint; + } + } + createdClsSpec.is_crud = true; + if (APISpec.allow_file_upload) { + createdClsSpec.creation_allow_file_upload = true; + } + // add creation params to fields + for (const i in APISpec.params) { + const param = APISpec.params[i]; + let fieldExists = false; + for (const j in createdClsSpec.fields) { + if (param.name === createdClsSpec.fields[j]['name']) { + fieldExists = true; + break; + } + } + if (!fieldExists) { + createdClsSpec.fields.push({ + name: param.name, + type: param.type, + api_name: param.api_name, + is_creation_field: true + }); + } + } + } + } + } + } + } // End of creation handling + + return specs; + } +};exports.default = processor; \ No newline at end of file diff --git a/lib/processors/FlaggingProcessor.js b/lib/processors/FlaggingProcessor.js new file mode 100644 index 00000000..834e8fe5 --- /dev/null +++ b/lib/processors/FlaggingProcessor.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _CodeGenNameConventions = require('./CodeGenNameConventions'); + +var _CodeGenNameConventions2 = _interopRequireDefault(_CodeGenNameConventions); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * This processor adds 'flags' in the APISpecs which are needed when generating + * the SDKs from templates. It does not change existing fields. It just add + * flags for our convenience. + * + * Currently it adds these flags: + * (1) 'cls_is_xxxx' flag + * (2) versioned features + * (3) 'is_method_get|post|delete' + * (4) 'is_node_api' 'return_single_node' + * (5) 'is_insights' 'is_insights_sync' 'is_insights_async' + * (6) 'context' + * (7) 'has_id' 'is_crud' + */ +const processor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + const versionedFeatures = metadata.versionedFeatures; + + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + APIClsSpec.name = clsName; + APIClsSpec['cls_is_' + clsName] = true; + // add versioned features for SDK + APIClsSpec.version = versionedFeatures; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + const method = APISpec.method; + APISpec.is_method_get = method === 'GET'; + APISpec.is_method_post = method === 'POST'; + APISpec.is_method_delete = method === 'DELETE'; + if (APISpec.name === 'get' || APISpec.name === 'update' || APISpec.name === 'delete') { + APISpec.is_node_api = true; + APISpec.return_single_node = true; + } + + if (APISpec.endpoint === 'insights') { + APISpec.is_insights = true; + if (APISpec.method === 'GET') { + APISpec.is_insights_sync = true; + } else if (APISpec.method === 'POST') { + APISpec.is_insights_async = true; + APISpec.return_single_node = true; + } + } else { + if (method === 'POST') { + APISpec.return_single_node = true; + } + } + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + if (fieldSpec.name === 'id') { + const parts = _CodeGenNameConventions2.default.parsePascalName(clsName).concat(['id']); + fieldSpec.context = parts.join('_'); + APIClsSpec.has_id = true; + APIClsSpec.is_crud = true; + } + } + } + + return specs; + } +}; + +exports.default = processor; \ No newline at end of file diff --git a/lib/processors/LanguageSpecificProcessor.js b/lib/processors/LanguageSpecificProcessor.js new file mode 100644 index 00000000..df57ea68 --- /dev/null +++ b/lib/processors/LanguageSpecificProcessor.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _CodeGenLanguages = require('./CodeGenLanguages'); + +var _CodeGenLanguages2 = _interopRequireDefault(_CodeGenLanguages); + +var _CodeGenNameConventions = require('./CodeGenNameConventions'); + +var _CodeGenNameConventions2 = _interopRequireDefault(_CodeGenNameConventions); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* + * This processor performs language specific transformations for the specs, as + * defined in codeGenLanguages. + */ +const processor = { + process(specs, metadata) { + const language = metadata.language || ''; + const languageDef = _CodeGenLanguages2.default[language]; + const APISpecs = specs.api_specs; + const enumMetadataMap = specs.enumMetadataMap; + + if (languageDef.preMustacheProcess) { + languageDef.preMustacheProcess(APISpecs, _CodeGenNameConventions2.default, enumMetadataMap); + } + + return specs; + } +}; + +exports.default = processor; \ No newline at end of file diff --git a/lib/processors/NamingConventionProcessor.js b/lib/processors/NamingConventionProcessor.js new file mode 100644 index 00000000..16c97e95 --- /dev/null +++ b/lib/processors/NamingConventionProcessor.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _CodeGenNameConventions = require('./CodeGenNameConventions'); + +var _CodeGenNameConventions2 = _interopRequireDefault(_CodeGenNameConventions); + +var _CodeGenLanguages = require('./CodeGenLanguages'); + +var _CodeGenLanguages2 = _interopRequireDefault(_CodeGenLanguages); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * This processor populates naming convention for the APISpecs. + * It parses the current value and adds different naming styles. + * + * The spec is translated like so + * @example + * + * {"name": "HelloWorld"} => + * + * { + * "name": "HelloWorld", + * "name:hyphen": "hello-world", + * "name:underscore": "hello_world", + * "name:pascal_case": "HelloWorld", + * "name:camel_case": "helloWorld", + * "name:upper_case": "HELLO_WORLD", + * "name:all_lower_case": "helloworld" + * } + */ +const processor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + const language = metadata.language || ''; + const languageDef = _CodeGenLanguages2.default[language]; + + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + if (APIClsSpec.creation_parent_class) { + _CodeGenNameConventions2.default.populateNameConventions(APIClsSpec, 'creation_parent_class', _CodeGenNameConventions2.default.parsePascalName(APIClsSpec.creation_parent_class)); + } + if (APIClsSpec.creation_method) { + _CodeGenNameConventions2.default.populateNameConventions(APIClsSpec, 'creation_method', _CodeGenNameConventions2.default.parseUnderscoreName(APIClsSpec.creation_method)); + } + } + + // add naming convention for enums + const enumMetadataMap = specs.enumMetadataMap; + for (const index in enumMetadataMap) { + const enumType = enumMetadataMap[index]; + const dedupchecker = {}; + if (enumType.node === 'AdReportRun') { + // We want all insights enums to be in AdsInsights, not AdReportRun + enumType.node = 'AdsInsights'; + } + const valuesWithNamingConvention = []; + for (const i in enumType.values) { + const value = enumType.values[i]; + if (!value || value === '') { + continue; + } + const entry = { value: value, is_irregular_name: false }; + if (languageDef.keywords.indexOf(value.toLowerCase()) > -1 || !value.match || !value.match(/^[a-zA-Z][a-zA-z0-9_]/)) { + entry.is_irregular_name = true; + } + _CodeGenNameConventions2.default.populateNameConventions(entry, 'value', _CodeGenNameConventions2.default.parseUnderscoreName(_CodeGenNameConventions2.default.removeIlligalChars(entry.value))); + if (!dedupchecker[entry.value.toUpperCase()]) { + dedupchecker[entry.value.toUpperCase()] = true; + valuesWithNamingConvention.push(entry); + } + } + _CodeGenNameConventions2.default.populateNameConventions(enumType, 'field_or_param', _CodeGenNameConventions2.default.parseUnderscoreName(enumType.field_or_param)); + if (enumType.node) { + _CodeGenNameConventions2.default.populateNameConventions(enumType, 'node', _CodeGenNameConventions2.default.parsePascalName(enumType.node)); + } + enumType.values_with_naming_convention = valuesWithNamingConvention; + } + + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + _CodeGenNameConventions2.default.populateNameConventions(APIClsSpec, 'name', _CodeGenNameConventions2.default.parsePascalName(APIClsSpec.name), 'cls:'); + + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + _CodeGenNameConventions2.default.populateNameConventions(APISpec, 'name', _CodeGenNameConventions2.default.parseUnderscoreName(APISpec.name), 'api:'); + + const params = APISpec.params || []; + for (const index in params) { + const paramSpec = params[index]; + _CodeGenNameConventions2.default.populateNameConventions(paramSpec, 'name', _CodeGenNameConventions2.default.parseUnderscoreName(paramSpec.name), 'param:'); + if (paramSpec.context) { + _CodeGenNameConventions2.default.populateNameConventions(paramSpec, 'context', _CodeGenNameConventions2.default.parseUnderscoreName(paramSpec.context)); + } + + if (languageDef.keywords.indexOf(paramSpec.name.toLowerCase()) > -1) { + paramSpec.is_keyword = true; + } + } + if (APISpecs[APISpec.return]) { + _CodeGenNameConventions2.default.populateNameConventions(APISpec, 'return', _CodeGenNameConventions2.default.parsePascalName(APISpec.return)); + } + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + _CodeGenNameConventions2.default.populateNameConventions(fieldSpec, 'name', _CodeGenNameConventions2.default.parseUnderscoreName(fieldSpec.name), 'field:'); + if (fieldSpec.context) { + _CodeGenNameConventions2.default.populateNameConventions(fieldSpec, 'context', _CodeGenNameConventions2.default.parseUnderscoreName(fieldSpec.context)); + } + if (languageDef.keywords.indexOf(fieldSpec.name.toLowerCase()) > -1 || !/^[$A-Z_][0-9A-Z_$]*$/i.test(fieldSpec.name)) { + // This is to mark field names that are either keywords or + // is not a valid identifier. We need special treatment to use + // them in codegen. + fieldSpec.is_irregular_name = true; + } + } + } + return specs; + } +}; + +exports.default = processor; \ No newline at end of file diff --git a/lib/processors/NodeEndpointHackProcessor.js b/lib/processors/NodeEndpointHackProcessor.js new file mode 100644 index 00000000..b5115bb2 --- /dev/null +++ b/lib/processors/NodeEndpointHackProcessor.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + + +/** + * We have special treatment for node APIs (i.e. GET/POST/DELETE on node) + * Ideally these should be generated from API Specgen, but the current + * status is that we use '#get' '#update' and '#delete' as name to indicate + * it's an node endpoint. + * + * This processor normalizes these endpoints. + * In addition, this processor also adds 'read_endpoint' to these nodes as + * specified in codegen/api_specs/{version}/SDKCodegen.json + */ +const processor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + const name = APISpec.name; + if (name === '#get') { + APISpec.endpoint = ''; + APISpec.method = 'GET'; + APISpec.name = 'get'; + APISpec.return = clsName; + APIClsSpec.has_get = true; + } else if (name === '#update') { + APISpec.endpoint = ''; + APISpec.method = 'POST'; + APISpec.name = 'update'; + } else if (name === '#delete') { + APISpec.endpoint = ''; + APISpec.method = 'DELETE'; + APISpec.name = 'delete'; + } + } + } + + // handling read_endpoints + const readEndpoints = metadata.mergedOverriding.read_endpoints; + for (const clsName in readEndpoints) { + if (APISpecs[clsName]) { + APISpecs[clsName]['read_endpoint'] = readEndpoints[clsName]; + } + } + return specs; + } +}; + +exports.default = processor; \ No newline at end of file diff --git a/lib/processors/NormalizationProcessor.js b/lib/processors/NormalizationProcessor.js new file mode 100644 index 00000000..aabddded --- /dev/null +++ b/lib/processors/NormalizationProcessor.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +// $FlowFixMe + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _pluralize = require('pluralize'); + +var _pluralize2 = _interopRequireDefault(_pluralize); + +var _CodeGenNameConventions = require('./CodeGenNameConventions'); + +var _CodeGenNameConventions2 = _interopRequireDefault(_CodeGenNameConventions); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const processor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + + let seenWords = []; + for (const clsName in APISpecs) { + seenWords = seenWords.concat(_CodeGenNameConventions2.default.parsePascalName(clsName)); + const APIClsSpec = APISpecs[clsName]; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + if (APISpec.return) { + seenWords = seenWords.concat(_CodeGenNameConventions2.default.parsePascalName(APISpec.return)); + } + } + } + _CodeGenNameConventions2.default.initDecodeDictionary(seenWords); + + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + const name = APISpec.name; + if (!name) { + const method = APISpec.method; + const parts = _CodeGenNameConventions2.default.parseEndpointName(APISpec.endpoint, _CodeGenNameConventions2.default.parsePascalName(APISpec.return), clsName); + + if (APISpec.endpoint === 'insights') { + if (APISpec.method === 'GET') { + APISpec.name = 'get_insights'; + } else if (APISpec.method === 'POST') { + APISpec.name = 'get_insights_async'; + } + } else { + if (method === 'GET') { + parts.unshift('get'); + } else if (method === 'POST') { + parts.unshift('create'); + // Hack for Business node because it has /adaccount and /adaccounts + if (clsName != 'Business' || APISpec.endpoint != 'adaccounts') { + parts[parts.length - 1] = (0, _pluralize2.default)(parts[parts.length - 1], 1); + } + } else if (method === 'DELETE') { + parts.unshift('delete'); + } else { + throw Error('invalid edge method: ' + method); + } + APISpec.name = parts.join('_'); + } + } + + const params = APISpec.params || []; + for (const index in params) { + const paramSpec = params[index]; + paramSpec.api_name = paramSpec.name; + } + } + + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + fieldSpec.api_name = fieldSpec.name; + // Add field that is normalized and has an underscore. + const fieldName = fieldSpec.name.split('.').join('_'); + fieldSpec['api_name:underscore_excluding_digit_suffix'] = isNaN(fieldName.charAt(0)) ? fieldName : 'value_' + fieldName; + } + } + + const enumMetadataMap = specs.enumMetadataMap; + for (const index in enumMetadataMap) { + const metadata = enumMetadataMap[index]; + if (!APISpecs[metadata.node]) { + metadata.node = null; + } + } + + return specs; + } +}; + +exports.default = processor; \ No newline at end of file diff --git a/lib/processors/ReferenceProcessor.js b/lib/processors/ReferenceProcessor.js new file mode 100644 index 00000000..a26c26d6 --- /dev/null +++ b/lib/processors/ReferenceProcessor.js @@ -0,0 +1,224 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _CodeGenNameConventions = require('./CodeGenNameConventions'); + +var _CodeGenNameConventions2 = _interopRequireDefault(_CodeGenNameConventions); + +var _CodeGenUtil = require('./CodeGenUtil'); + +var _CodeGenUtil2 = _interopRequireDefault(_CodeGenUtil); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * This processor handles the references between nodes and enums. + */ +const ReferenceProcessor = { + process(specs, metadata) { + const APISpecs = specs.api_specs; + const enumMetadataMap = specs.enumMetadataMap; + + // add enum reference info into class specs + for (const index in enumMetadataMap) { + const enumType = enumMetadataMap[index]; + const nodeName = enumType.node; + if (nodeName && APISpecs[nodeName]) { + const clsSpec = APISpecs[nodeName]; + if (!clsSpec.api_spec_based_enum_reference) { + clsSpec.api_spec_based_enum_reference = []; + clsSpec.api_spec_based_enum_list = {}; + } + if (clsSpec.api_spec_based_enum_list[enumType.field_or_param]) { + // already exists. do nothing + } else { + clsSpec.api_spec_based_enum_reference.push(enumType); + clsSpec.api_spec_based_enum_list[enumType.field_or_param] = true; + } + + // todo: move all flagging logics into FlaggingProcessor + if (nodeName === 'AdsInsights' && enumType.field_or_param === 'breakdowns') { + clsSpec.breakdowns = enumType; + } + } + } + + for (const clsName in APISpecs) { + const APIClsSpec = APISpecs[clsName]; + _CodeGenNameConventions2.default.populateNameConventions(APISpecs[clsName], 'names', _CodeGenNameConventions2.default.parsePascalName(clsName)); + + // Initialize references based on API return types + const references = {}; + if (APIClsSpec.references) { + APIClsSpec.references.forEach(ref => { + const ref_cls_name = ref; + references[ref_cls_name] = true; + }); + } + + // Initialize field references + const fieldReferences = {}; + + // Process API class + APIClsSpec.node = []; + APIClsSpec.edges = []; + for (const index in APIClsSpec.apis) { + const APISpec = APIClsSpec.apis[index]; + const apiReferencedEnumTypes = {}; + const apiClassReferences = {}; + + let hasParamFields = false; + const params = APISpec.params || []; + for (const index in params) { + const paramSpec = params[index]; + if (paramSpec.name === 'fields') { + hasParamFields = true; + } + if (paramSpec.type) { + paramSpec['type:short'] = paramSpec.type; + const baseType = _CodeGenUtil2.default.getBaseType(paramSpec.type); + if (APISpecs[baseType]) { + APISpecs[baseType]['can_be_data_type'] = true; + } + if (enumMetadataMap[baseType]) { + const enumParamName = paramSpec.name + '_enum'; + const metadata = enumMetadataMap[baseType]; + const enumType = { + name: enumParamName, + metadata: metadata + }; + _CodeGenNameConventions2.default.populateNameConventions(enumType, 'name', _CodeGenNameConventions2.default.parseUnderscoreName(enumType.name)); + paramSpec['type:short'] = paramSpec.type.replace(baseType, enumParamName); + apiReferencedEnumTypes[baseType] = enumType; + } + } + } + + // Standardize endpoints starting with "/" + if (APISpec.endpoint.charAt(0) !== '/') { + APISpec.endpoint = '/' + APISpec.endpoint; + } + + // Resolve return types + const returnClsSpec = APISpecs[APISpec.return]; + if (returnClsSpec) { + // Add "fields" field + if (APISpec.method === 'GET' && !hasParamFields) { + APISpec.param_fields = returnClsSpec.fields.filter(field => { + return !field.is_creation_field; + }); + } else { + APISpec.param_fields = false; + } + } else { + delete APISpec.return; + } + + if (APISpec.return) { + references[APISpec.return] = true; + apiClassReferences[APISpec.return] = true; + + _CodeGenNameConventions2.default.populateNameConventions(APISpec, 'return', _CodeGenNameConventions2.default.parsePascalName(APISpec.return)); + } + + if (Object.keys(apiReferencedEnumTypes).length) { + const apiReferencedEnumList = []; + for (const key in apiReferencedEnumTypes) { + apiReferencedEnumList.push(apiReferencedEnumTypes[key]); + const cls = apiReferencedEnumTypes[key]['metadata']['node']; + if (cls) { + apiClassReferences[cls] = true; + } + } + APISpec.referred_enums = apiReferencedEnumList; + } + + APISpec.referred_classes = []; + for (const refName in apiClassReferences) { + if (refName !== clsName) { + const refObj = {}; + _CodeGenNameConventions2.default.populateNameConventions(refObj, 'name', _CodeGenNameConventions2.default.parsePascalName(refName), 'api-ref:'); + APISpec.referred_classes.push(refObj); + } + } + + if (APISpec.is_node_api === true) { + APIClsSpec.node.push(APISpec); + } else { + APIClsSpec.edges.push(APISpec); + } + } + + // Process references + APIClsSpec.references = []; + for (const refName in references) { + // no self-reference + // no self-reference after overrides + if (refName !== clsName && refName !== APIClsSpec.name) { + const refObj = {}; + _CodeGenNameConventions2.default.populateNameConventions(refObj, 'name', _CodeGenNameConventions2.default.parsePascalName(refName), 'ref:'); + APIClsSpec.references.push(refObj); + } + } + + // Pure data type or not + const isDataType = !APIClsSpec.apis || APIClsSpec.apis.length == 0; + APIClsSpec.data_type = isDataType; + + // Process fields of current object + let hasIdField = false; + for (const index in APIClsSpec.fields) { + const fieldSpec = APIClsSpec.fields[index]; + if (fieldSpec.name === 'id') { + hasIdField = true; + fieldSpec.is_id_field = true; + } + const referenceType = getReferenceType(fieldSpec.type, APISpecs); + if (referenceType !== null && referenceType !== undefined) { + fieldReferences[referenceType] = true; + } + } + + // Process references + APIClsSpec.field_references = []; + for (const refName in fieldReferences) { + // no self-reference + // no self-reference after overrihs + if (refName !== clsName && refName !== APIClsSpec.name) { + const refObj = {}; + _CodeGenNameConventions2.default.populateNameConventions(refObj, 'name', _CodeGenNameConventions2.default.parsePascalName(refName), 'ref:'); + APIClsSpec.field_references.push(refObj); + } + } + + if (!isDataType && !hasIdField) { + throw Error('Root nodes ' + clsName + ' must have the "id" field!'); + } + } + + return specs; + } +}; + +const getReferenceType = (type, APISpecs) => { + if (type) { + const referenceType = _CodeGenUtil2.default.getBaseType(type); + if (referenceType in APISpecs) { + return referenceType; + } + } + + return null; +}; + +exports.default = ReferenceProcessor; diff --git a/lib/processors/SpecOverridingProcessor.js b/lib/processors/SpecOverridingProcessor.js new file mode 100644 index 00000000..38d55a3d --- /dev/null +++ b/lib/processors/SpecOverridingProcessor.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +// $FlowFixMe + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _jsonpathPlus = require('jsonpath-plus'); + +var _jsonpathPlus2 = _interopRequireDefault(_jsonpathPlus); + +var _merge = require('merge'); + +var _CodeGenLanguages = require('./CodeGenLanguages'); + +var _CodeGenLanguages2 = _interopRequireDefault(_CodeGenLanguages); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Apply manual spec overriding from + * codegen/api_specs/SDKCodegen.json + */ + + +// $FlowFixMe +const SpecOverridingProcessor = { + process(specs, metadata) { + const language = metadata.language || ''; + const flags = metadata.mergedOverriding.flags; + let APISpecs = specs.api_specs; + const languageDef = _CodeGenLanguages2.default[language]; + const specOverriding = metadata.mergedOverriding.spec_overriding; + for (const clsName in specOverriding) { + for (const jsonPath in specOverriding[clsName]) { + const value = specOverriding[clsName][jsonPath]; + const $ = APISpecs[clsName] || {}; + const evalPathes = jsonPath === '$' ? ['$'] : _jsonpathPlus2.default.eval($, jsonPath, { resultType: 'path' }); + if (evalPathes.length === 0) { + throw new Error('MUST_FIX: cannot find JSON path need to be patched.\n' + clsName + '::' + jsonPath); + } + + if (value === null) { + // Delete + evalPathes.forEach(path => { + // Find the parent path before it + // Since all paths are of the form $[..][..][..] we only need + // to remove the last [..] + const lastBracketPos = path.lastIndexOf('['); + if (lastBracketPos != -1) { + const parentPath = path.substring(0, lastBracketPos); + const evalParentValue = eval(parentPath); + if (evalParentValue instanceof Array) { + // since evalParentValue is an array, accessor must be a number + const accessor = path.substring(lastBracketPos + 1, path.length - 1); + if (isNaN(Number(accessor))) { + throw new Error('Accessor for last element in array must ' + 'be integer but instead ' + accessor); + } + evalParentValue.splice(Number(accessor), 1); + } else { + eval('delete ' + path); + } + } + }); + } else { + evalPathes.forEach(path => { + const evalValue = eval(path); + if (evalValue instanceof Array) { + if (value instanceof Array) { + // Merge array + evalValue.concat(value); + } else { + throw new Error('MUST_FIX: value must be array while path is array.\n' + clsName + '::' + jsonPath); + } + } else if (evalValue instanceof Object) { + if (value instanceof Object) { + // Merge object + (0, _merge.recursive)(evalValue, value); + } else { + throw new Error('MUST_FIX: value must be object while path is object.\n' + clsName + '::' + jsonPath); + } + } else { + eval(path + '=value'); + } + }); + } + APISpecs[clsName] = $; + } + } + if (flags) { + for (const clsName in APISpecs) { + const cls_flags = flags[clsName]; + if (cls_flags) { + for (const i in cls_flags) { + APISpecs[clsName][cls_flags[i]] = true; + } + } + } + } + if (languageDef.specOverrideProcessing) { + APISpecs = languageDef.specOverrideProcessing(APISpecs); + } + return specs; + } +};exports.default = SpecOverridingProcessor; \ No newline at end of file diff --git a/lib/renderers/DebugJsonRenderer.js b/lib/renderers/DebugJsonRenderer.js new file mode 100644 index 00000000..4a609f46 --- /dev/null +++ b/lib/renderers/DebugJsonRenderer.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * strict-local + */ + +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const DebugJsonRenderer = { + render(specs, language, version, outputDir, cleandir) { + const outputFile = _path2.default.join(outputDir, 'compiled.json'); + _fs2.default.writeFileSync(outputFile, JSON.stringify(specs.APISpecs, null, 2)); + } +}; + +exports.default = DebugJsonRenderer; \ No newline at end of file diff --git a/lib/renderers/MustacheRenderer.js b/lib/renderers/MustacheRenderer.js new file mode 100644 index 00000000..6989b335 --- /dev/null +++ b/lib/renderers/MustacheRenderer.js @@ -0,0 +1,110 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + * + */ + +'use strict'; + +// $FlowFixMe + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _fsExtra = require('fs-extra'); + +var _fsExtra2 = _interopRequireDefault(_fsExtra); + +var _mustache = require('mustache'); + +var _mustache2 = _interopRequireDefault(_mustache); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _CodeGenLanguages = require('../processors/CodeGenLanguages'); + +var _CodeGenLanguages2 = _interopRequireDefault(_CodeGenLanguages); + +var _Utils = require('./Utils'); + +var _Utils2 = _interopRequireDefault(_Utils); + +var _Utils3 = require('../common/Utils'); + +var _Utils4 = _interopRequireDefault(_Utils3); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const MustacheRenderer = { + render(specs, language, version, outputDir, cleandir) { + const APISpecs = specs.APISpecs; + const SDKConfig = specs.SDKConfig; + + const codegenRootDir = _path2.default.resolve(__dirname, '..', '..'); + const templateDir = _path2.default.resolve(codegenRootDir, 'templates', language); + const sdkRootPath = _path2.default.resolve(outputDir); + + // load the mustache templates + const loadedTemplates = _Utils2.default.loadTemplates(templateDir); + const mainTemplates = loadedTemplates.mainTemplates; + const partialTemplates = loadedTemplates.partialTemplates; + const versionedTemplates = loadedTemplates.versionedTemplates; + const filesNeedCopy = loadedTemplates.filesNeedCopy; + + // clean up the folder + if (cleandir === undefined || cleandir.length == 0 || !_fsExtra2.default.existsSync(sdkRootPath)) { + _Utils2.default.removeRecursiveSync(sdkRootPath, language); + _Utils2.default.mkdirsSync(sdkRootPath); + } else { + for (const d of cleandir) { + const tmp = _path2.default.resolve(sdkRootPath, d); + console.log(tmp); + _Utils2.default.removeRecursiveSync(tmp, language); + _Utils2.default.mkdirsSync(tmp); + } + } + + // Copy the common folder + console.log('Generating ' + language + ' SDK in ' + outputDir + '...'); + + // @fb-only + // @fb-only + + // Generate code with mustache templates + for (const nodeName in APISpecs) { + const APIClsSpec = APISpecs[nodeName]; + _Utils2.default.fillMainTemplates(mainTemplates, partialTemplates, APIClsSpec, language, _CodeGenLanguages2.default, sdkRootPath); + } + + const depreciationSign = _Utils4.default.codeGenFileDepreciationSign; + const apiSpecArray = []; + for (const nodeName in APISpecs) { + const APIClsSpec = APISpecs[nodeName]; + apiSpecArray.push(APIClsSpec); + } + SDKConfig.api_specs = apiSpecArray; + versionedTemplates.forEach(value => { + const fileContent = _mustache2.default.render(value.content, SDKConfig, {}); + if (fileContent.trim().indexOf(depreciationSign + '\n')) { + const destDir = sdkRootPath + value.dir; + _Utils2.default.mkdirsSync(destDir); + _fsExtra2.default.writeFileSync(destDir + value.file, fileContent); + } + }); + + // Copy static files + filesNeedCopy.forEach(value => { + const destDir = sdkRootPath + value.dir; + _Utils2.default.mkdirsSync(destDir); + + _fsExtra2.default.copy(templateDir + value.dir + value.file, destDir + value.file); + }); + console.log(language + ' SDK generated in ' + outputDir); + } +}; + +exports.default = MustacheRenderer; diff --git a/lib/renderers/Utils.js b/lib/renderers/Utils.js new file mode 100644 index 00000000..50c0e800 --- /dev/null +++ b/lib/renderers/Utils.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + */ + +'use strict'; + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _fsExtra = require('fs-extra'); + +var _fsExtra2 = _interopRequireDefault(_fsExtra); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _mustache = require('mustache'); + +var _mustache2 = _interopRequireDefault(_mustache); + +var _version_path = require('../../api_specs/version_path.json'); + +var _version_path2 = _interopRequireDefault(_version_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const PATH_SKIP_AUTOGEN = [/^(.*)\/[sS]erver[_]?[sS]ide\/(.*)$/, /^(.*)\/\.travis.yml$/, /^(.*)\/\.github\/workflows\/(.*).yml$/, /^(.*)\/\keyring.gpg.enc$/, /^(.*)\/\.travis.settings.xml$/, /^(.*)\/facebook_business\/test\/(.*)$/, /^(.*)\/CHANGELOG.md$/, /^(.*)\/[bB]usiness[dD]ata[aA][pP][iI]\/(.*)$/, /^(.*)\/[sS]ignal\/(.*)$/, /^(.*)\/README.md$/, /^(.*)\/release.sh$/, /^(.*)\/\.flowconfig$/, /^(.*)\/facebookbusiness.gemspec$/, /^(.*)\/Gemfile(.*)$/, /^(.*)\/Rakefile$/, /^(.*)\/facebookbusiness.gemspec$/, /^(.*)\/customtype$/]; +const Utils = { + loadTemplates(templateDir) { + // 1. load codegen main templates + // 2. load codegen partial templates + // 3. discover files need to copy to the sdk folder + const mainTemplates = []; + const partialTemplates = {}; + const versionedTemplates = []; + const filesNeedCopy = []; + + const walkTemplateDir = dir => { + const relativeDir = dir.substring(templateDir.length) + _path2.default.sep; + _fs2.default.readdirSync(dir).forEach(file => { + const fullPath = _path2.default.join(dir, file); + + const stat = _fs2.default.statSync(fullPath); + if (stat.isDirectory()) { + walkTemplateDir(fullPath); + } else if (stat.isFile()) { + let match = file.match(/^(.*)\.mustache$/); + if (match) { + const name = match[1]; + if (name === 'codegen') { + // Main templates + mainTemplates.push({ + dir: relativeDir, + content: _fs2.default.readFileSync(fullPath, 'utf8') + }); + } else { + match = name.match(/^(.*)\.versioned$/); + if (match) { + // Config template ends with .versioned.mustache + versionedTemplates.push({ + content: _fs2.default.readFileSync(fullPath, 'utf8'), + dir: relativeDir, + file: match[1] + }); + } else { + // Partial templates + partialTemplates[relativeDir + name] = _fs2.default.readFileSync(fullPath, 'utf8'); + } + } + } else { + filesNeedCopy.push({ + dir: relativeDir, + file: file + }); + } + } + }); + }; + walkTemplateDir(templateDir); + return { + mainTemplates: mainTemplates, + partialTemplates: partialTemplates, + versionedTemplates: versionedTemplates, + filesNeedCopy: filesNeedCopy + }; + }, + + mkdirsSync(dir) { + let dirPath = ''; + const dirs = dir.split(_path2.default.sep); + while (dirs.length > 0) { + dirPath = dirPath + _path2.default.sep + dirs.shift(); + if (!_fs2.default.existsSync(dirPath)) { + _fs2.default.mkdirSync(dirPath); + } + } + }, + + fillMainTemplates(mainTemplates, partialTemplates, clsSpec, language, codeGenLanguages, rootPath) { + mainTemplates = this.preProcessMainTemplates(mainTemplates, clsSpec); + mainTemplates.forEach(template => { + const languageDef = codeGenLanguages[language]; + let filenameToCodeMap = {}; + + if (languageDef.generateFilenameToCodeMap) { + filenameToCodeMap = languageDef.generateFilenameToCodeMap(clsSpec, template, partialTemplates); + } else { + filenameToCodeMap = this.generateSimpleFilenameToCodeMap(clsSpec, template, partialTemplates, languageDef); + } + + for (const filename in filenameToCodeMap) { + const code = filenameToCodeMap[filename]; + const outputPath = _path2.default.join(rootPath, template.dir); + this.mkdirsSync(outputPath); + _fs2.default.writeFileSync(_path2.default.join(outputPath, filename), code); + } + }); + }, + + generateSimpleFilenameToCodeMap(clsSpec, template, partialTemplates, languageDef) { + const filenameToCodeMap = {}; + const filename = languageDef.formatFileName(clsSpec, template); + let code = _mustache2.default.render(template.content, clsSpec, partialTemplates); + + if (languageDef.postProcess) { + code = languageDef.postProcess(code); + } + + if (code && code.length > 0) { + filenameToCodeMap[filename] = code; + } + + return filenameToCodeMap; + }, + + preProcessMainTemplates(mainTemplates, clsSpec) { + return mainTemplates.map(template => { + const newTemplate = JSON.parse(JSON.stringify(template)); + newTemplate.content = newTemplate.content.replace(/{{\s*>.*(%([a-zA-Z:_]+)%).*}}/gi, + /** + * @param {string} m + * @param {string} p1 + * @param {string} p2 + */ + (m, p1, p2) => m.replace(p1, clsSpec[p2])); + return newTemplate; + }); + }, + + removeRecursiveSync(dir, language) { + for (var i = 0, len = PATH_SKIP_AUTOGEN.length; i < len; i++) { + let match = dir.match(PATH_SKIP_AUTOGEN[i]); + if (match) { + return false; + } + } + + let version_file = _version_path2.default[language]['base_path'] + '/' + _version_path2.default[language]['file_path']; + let match = dir.includes(version_file); + if (match) { + return false; + } + + let stats; + try { + stats = _fs2.default.lstatSync(dir); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + return false; + } + + if (stats.isDirectory()) { + let delete_all = true; + _fs2.default.readdirSync(dir).forEach(file => delete_all &= Utils.removeRecursiveSync(_path2.default.join(dir, file), language)); + if (delete_all) { + _fs2.default.rmdirSync(dir); + } else { + return false; + } + } else { + _fs2.default.unlinkSync(dir); + } + return true; + }, + + copyRecursiveSync(srcDir, destDir) { + const srcDirStats = _fs2.default.statSync(srcDir); + if (srcDirStats.isDirectory()) { + try { + _fs2.default.mkdirSync(destDir); + } catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } + } + _fs2.default.readdirSync(srcDir).forEach(file => Utils.copyRecursiveSync(_path2.default.join(srcDir, file), _path2.default.join(destDir, file))); + } else { + try { + _fsExtra2.default.copySync(srcDir, destDir); + } catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } + } + } + } +}; + +module.exports = Utils; diff --git a/templates/csharp/APIConfig.cs.versioned.mustache b/templates/csharp/APIConfig.cs.versioned.mustache new file mode 100644 index 00000000..13e62faf --- /dev/null +++ b/templates/csharp/APIConfig.cs.versioned.mustache @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to + * use, copy, modify, and distribute this software in source code or binary + * form for use in connection with the web services and APIs provided by + * Facebook. + * + * As with any software that integrates with the Facebook platform, your use + * of this software is subject to the Facebook Developer Principles and + * Policies [http://developers.facebook.com/policy/]. This copyright notice + * shall be included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +using System; +using System.Text; + +namespace Facebook.Ads.SDK +{ + public class APIConfig { + public const string DEFAULT_API_VERSION = "{{api_version}}"; + public const string DEFAULT_API_BASE = "https://graph.facebook.com"; + public const string USER_AGENT = "fb-csharp-ads-api-sdk-{{api_version}}"; + } +} diff --git a/templates/csharp/APIContext.cs b/templates/csharp/APIContext.cs new file mode 100644 index 00000000..71e82367 --- /dev/null +++ b/templates/csharp/APIContext.cs @@ -0,0 +1,105 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. All rights reserved. +* +* You are hereby granted a non-exclusive, worldwide, royalty-free license to +* use, copy, modify, and distribute this software in source code or binary +* form for use in connection with the web services and APIs provided by +* Facebook. +* +* As with any software that integrates with the Facebook platform, your use +* of this software is subject to the Facebook Developer Principles and +* Policies [http://developers.facebook.com/policy/]. This copyright notice +* shall be included in all copies or substantial portions of the software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +* +*/ + +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Facebook.Ads.SDK +{ + public class APIContext + { + public const string DEFAULT_API_BASE = APIConfig.DEFAULT_API_BASE; + public const string DEFAULT_API_VERSION = APIConfig.DEFAULT_API_VERSION; + protected bool isDebug = false; + //protected PrintStream logger = System.out; + + protected APIContext(string endpointBase, string version, string accessToken, string appSecret) + { + this.Version = version; + this.EndpointBase = endpointBase; + this.AccessToken = accessToken; + this.AppSecret = appSecret; + } + + public APIContext(string accessToken) + : this(DEFAULT_API_BASE, DEFAULT_API_VERSION, accessToken, null) + { + } + + public APIContext(string accessToken, string appSecret) + : this(DEFAULT_API_BASE, DEFAULT_API_VERSION, accessToken, appSecret) + { + } + + public string EndpointBase { get; private set; } + + public string AccessToken { get; private set; } + + public string AppSecret { get; private set; } + + public string AppSecretProof + { + get { return APIContext.sha256(this.AppSecret, this.AccessToken); } + } + + public string Version { get; private set; } + + public bool IsDebug { get; set; } + + // public PrintStream Logger() { } + + public bool hasAppSecret() + { + return this.AppSecret != null; + } + + public static string sha256(string secret, string message) + { + try + { + var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(message)); + return ToHex(bytes); + } + catch (Exception) + { + return null; + } + } + + private static String ToHex(byte[] bytes) + { + var sb = new StringBuilder(); + foreach (byte b in bytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + + //public void log(String s) { + // if (isDebug && logger != null) logger.println(s); + //} + } +} diff --git a/templates/csharp/APIException.cs b/templates/csharp/APIException.cs new file mode 100644 index 00000000..9b5d3745 --- /dev/null +++ b/templates/csharp/APIException.cs @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to + * use, copy, modify, and distribute this software in source code or binary + * form for use in connection with the web services and APIs provided by + * Facebook. + * + * As with any software that integrates with the Facebook platform, your use + * of this software is subject to the Facebook Developer Principles and + * Policies [http://developers.facebook.com/policy/]. This copyright notice + * shall be included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +using System; +using System.Runtime.Serialization; + +namespace Facebook.Ads.SDK +{ + [Serializable()] + public class APIException : Exception, IAPIResponse + { + public APIException() + { + } + + public APIException(string message) + : base(message) + { + } + + public APIException(string message, Exception innerException) + : base (message, innerException) + { + } + + protected APIException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + public APIException Exception + { + get { return this; } + } + + public APINode Head() + { + return null; + } + + public string ToRawResponse() + { + return this.Message; + } + + public override string ToString() + { + return this.ToRawResponse(); + } + } +} diff --git a/templates/csharp/APINode.cs b/templates/csharp/APINode.cs new file mode 100644 index 00000000..eba0dc36 --- /dev/null +++ b/templates/csharp/APINode.cs @@ -0,0 +1,80 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. All rights reserved. +* +* You are hereby granted a non-exclusive, worldwide, royalty-free license to +* use, copy, modify, and distribute this software in source code or binary +* form for use in connection with the web services and APIs provided by +* Facebook. +* +* As with any software that integrates with the Facebook platform, your use +* of this software is subject to the Facebook Developer Principles and +* Policies [http://developers.facebook.com/policy/]. This copyright notice +* shall be included in all copies or substantial portions of the software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +* +*/ + +using System.Collections.Generic; +using System.Web.Script.Serialization; + +namespace Facebook.Ads.SDK +{ + public class APINode : IAPIResponse + { + protected string rawValue = null; + + public APIContext Context + { + get; set; + } + + // TODO: This is a hack to allow raw dictionary data access. + public Dictionary Data + { + get; + protected set; + } + + public APIException Exception + { + get { return null; } + } + + public string ToRawResponse() + { + return rawValue; + } + + public APINode Head() + { + return null; + } + + public static APINodeList ParseResponse(string json, APIContext context, APIRequest request) + { + // TODO: implement + return null; + } + + public static APINode LoadFromDictionary(dynamic dict) + { + var node = new APINode(); + node.Data = dict; + return node; + } + + public static dynamic SerializeJSON(string json) + { + var serializer = new JavaScriptSerializer(); + serializer.MaxJsonLength = 20971520; // increase the limit to 40MB. + return serializer.DeserializeObject(json); + } + } +} diff --git a/templates/csharp/APINodeList.cs b/templates/csharp/APINodeList.cs new file mode 100644 index 00000000..4ea8c77c --- /dev/null +++ b/templates/csharp/APINodeList.cs @@ -0,0 +1,58 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. All rights reserved. +* +* You are hereby granted a non-exclusive, worldwide, royalty-free license to +* use, copy, modify, and distribute this software in source code or binary +* form for use in connection with the web services and APIs provided by +* Facebook. +* +* As with any software that integrates with the Facebook platform, your use +* of this software is subject to the Facebook Developer Principles and +* Policies [http://developers.facebook.com/policy/]. This copyright notice +* shall be included in all copies or substantial portions of the software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +* +*/ + +using System; +using System.Collections.Generic; + +namespace Facebook.Ads.SDK +{ + public class APINodeList : List, IAPIResponse where T : APINode + { + private string before; + private string after; + private APIRequest request; + private string rawValue; + + public APINodeList(APIRequest request, string rawValue) + { + this.request = request; + this.rawValue = rawValue; + } + + public APIException Exception + { + get { return null; } + } + + public APINode Head() + { + return this.Count > 0 ? this[0] : null; + } + + public string ToRawResponse() + { + return rawValue; + } + + } +} diff --git a/templates/csharp/APIRequest.cs b/templates/csharp/APIRequest.cs new file mode 100644 index 00000000..e6de3d62 --- /dev/null +++ b/templates/csharp/APIRequest.cs @@ -0,0 +1,304 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. All rights reserved. +* +* You are hereby granted a non-exclusive, worldwide, royalty-free license to +* use, copy, modify, and distribute this software in source code or binary +* form for use in connection with the web services and APIs provided by +* Facebook. +* +* As with any software that integrates with the Facebook platform, your use +* of this software is subject to the Facebook Developer Principles and +* Policies [http://developers.facebook.com/policy/]. This copyright notice +* shall be included in all copies or substantial portions of the software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +* +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; + +namespace Facebook.Ads.SDK +{ + public abstract class APIRequest where T : APINode + { + protected string nodeId; + protected string endpoint; + protected string method; + protected List paramNames; + protected IResponseParser parser; + protected IDictionary parameters = new Dictionary(); + + protected List returnFields; + public const string USER_AGENT = APIConfig.USER_AGENT; + protected static IDictionary fileToContentTypeMap = new Dictionary + { + {".atom", "application/atom+xml"}, + {".rss", "application/rss+xml"}, + {".xml", "application/xml"}, + {".csv", "text/csv"}, + {".txt", "text/plain"} + }; + + public APIRequest(APIContext context, string nodeId, string endpoint, string method) + : this(context, nodeId, endpoint, method, null, null) + { + } + + public APIRequest(APIContext context, string nodeId, string endpoint, string method, IResponseParser parser) + : this(context, nodeId, endpoint, method, null, parser) + { + } + + public APIRequest(APIContext context, string nodeId, string endpoint, string method, IEnumerable paramNames) + : this(context, nodeId, endpoint, method, paramNames, null) + { + } + + public APIRequest(APIContext context, string nodeId, string endpoint, string method, IEnumerable paramNames, IResponseParser parser) + { + this.Context = context; + this.nodeId = nodeId; + this.endpoint = endpoint; + this.method = method; + this.paramNames = new List(paramNames); + this.parser = parser; + } + + public abstract bool IsCollectionResponse + { + get; + } + + public APINodeList LastResponse + { + get; + protected set; + } + + public APIContext Context + { + get; + protected set; + } + + protected abstract APINodeList ParseResponse(string response); + + public APINodeList Execute() + { + return this.Execute(new Dictionary()); + } + + public APINodeList Execute(Dictionary extraParams) + { + this.LastResponse = this.ParseResponse(this.SendRequest(extraParams)); + return this.LastResponse; + } + + private string GetApiUrl() + { + return this.Context.EndpointBase + "/" + this.Context.Version + "/" + this.nodeId + this.endpoint; + } + + protected string SendRequest(Dictionary extraParams) + { + // extraParams are one-time params for this call, + // so that the APIRequest can be reused later on. + string response = null; + if ("GET".Equals(this.method, StringComparison.Ordinal)) + { + response = SendGet(this.GetApiUrl(), GetAllParams(extraParams), this.Context); + } + else if ("POST".Equals(this.method, StringComparison.Ordinal)) + { + response = SendPost(this.GetApiUrl(), GetAllParams(extraParams), this.Context); + } + else if ("DELETE".Equals(this.method, StringComparison.Ordinal)) + { + response = SendDelete(this.GetApiUrl(), GetAllParams(extraParams), this.Context); + } + else + { + var message = "Unsupported http method. Currently only GET, POST, and DELETE are supported"; + throw new APIException(message, new ArgumentException(message)); + } + return response; + } + + // HTTP GET request + private static string SendGet(string apiUrl, Dictionary allParams, APIContext context) + { + var sb = new StringBuilder(apiUrl); + bool firstEntry = true; + foreach (var entry in allParams) + { + sb.Append(firstEntry ? "?" : "&") + .Append(WebUtility.UrlEncode(entry.Key)) + .Append("=") + .Append(WebUtility.UrlEncode(ValueToString(entry.Value))); + firstEntry = false; + } + using (var client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString()); + request.Headers.Add("UserAgent", USER_AGENT); + //request.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + // TODO: C# is more naturally to use Async Pattern. We shall later change this as + // This can potentially block the UI thread and create deadlock. + var response = client.SendAsync(request).Result; + return response.Content.ReadAsStringAsync().Result; + //return client.GetStringAsync(sb.ToString()).Result; + } + } + + private static string ContentTypeForFile(FileInfo file) + { + var extension = file.Extension.ToLowerInvariant(); + string contentType; + fileToContentTypeMap.TryGetValue(extension, out contentType); + return contentType; + } + + // HTTP POST request + private static string SendPost(string apiUrl, Dictionary allParams, APIContext context) + { + using (var client = new HttpClient()) + { + using (var content = new MultipartFormDataContent()) + { + foreach (var entry in allParams) + { + var fileValue = entry.Value as FileInfo; + if (fileValue != null) + { + using (FileStream fs = fileValue.OpenRead()) + { + content.Add(new StreamContent(fs), fileValue.Name, fileValue.Name); + } + } + else + { + content.Add( + new StringContent(entry.Value.ToString(), Encoding.UTF8), + entry.Key); + } + } + using (var response = client.PostAsync(apiUrl, content).Result) + { + return response.Content.ReadAsStringAsync().Result; + } + } + } + } + + private static string SendDelete(string apiUrl, Dictionary allParams, APIContext context) + { + var sb = new StringBuilder(apiUrl); + bool firstEntry = true; + foreach (var entry in allParams) + { + sb.Append(firstEntry ? "?" : "&") + .Append(WebUtility.UrlEncode(entry.Key)) + .Append("=") + .Append(WebUtility.UrlEncode(ValueToString(entry.Value))); + firstEntry = false; + } + using (var client = new HttpClient()) + { + // TODO: C# is more naturally to use Async Pattern. We shall later change this as + // This can potentially block the UI thread and create deadlock. + using (var response = client.DeleteAsync(sb.ToString()).Result) + { + return response.Content.ReadAsStringAsync().Result; + } + } + } + + private static string ValueToString(object input) + { + if (input == null) + { + return "null"; + } + else if (input is IDictionary + || input is IDictionary + || input is IList + || input is IList) + { + return new JavaScriptSerializer().Serialize(input); + } + else + { + return input.ToString(); + } + } + + private Dictionary GetAllParams(Dictionary extraParams) + { + var allParams = new Dictionary(this.parameters); + if (extraParams != null) + { + foreach (var pair in extraParams) + { + allParams[pair.Key] = pair.Value; + } + } + allParams["access_token"] = this.Context.AccessToken; + if (this.Context.hasAppSecret()) + { + allParams["appsecret_proof"] = this.Context.AppSecretProof; + } + if (returnFields != null) + { + allParams["fields"] = JoinStringList(returnFields); + } + return allParams; + } + + private static string JoinStringList(List list) + { + if (list == null) return ""; + return String.Join(",", list.ToArray()); + } + + protected void SetParamInternal(string parameter, object value) + { + this.parameters[parameter] = value; + } + + protected void SetParamsInternal(IDictionary parameters) + { + this.parameters = parameters; + } + + protected void RequestFieldInternal(string field, bool value) + { + if (this.returnFields == null) + { + returnFields = new List(); + } + if (value == true && !this.returnFields.Contains(field)) + { + returnFields.Add(field); + } + else + { + returnFields.Remove(field); + } + } + } +} diff --git a/templates/csharp/IAPIResponse.cs b/templates/csharp/IAPIResponse.cs new file mode 100644 index 00000000..00bbcab1 --- /dev/null +++ b/templates/csharp/IAPIResponse.cs @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to + * use, copy, modify, and distribute this software in source code or binary + * form for use in connection with the web services and APIs provided by + * Facebook. + * + * As with any software that integrates with the Facebook platform, your use + * of this software is subject to the Facebook Developer Principles and + * Policies [http://developers.facebook.com/policy/]. This copyright notice + * shall be included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +namespace Facebook.Ads.SDK +{ + public interface IAPIResponse + { + APIException Exception { get; } + string ToRawResponse(); + APINode Head(); + } +} diff --git a/templates/csharp/IResponseParser.cs b/templates/csharp/IResponseParser.cs new file mode 100644 index 00000000..df1f2370 --- /dev/null +++ b/templates/csharp/IResponseParser.cs @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to + * use, copy, modify, and distribute this software in source code or binary + * form for use in connection with the web services and APIs provided by + * Facebook. + * + * As with any software that integrates with the Facebook platform, your use + * of this software is subject to the Facebook Developer Principles and + * Policies [http://developers.facebook.com/policy/]. This copyright notice + * shall be included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +namespace Facebook.Ads.SDK +{ + public interface IResponseParser where T : APINode + { + APINodeList ParseResponse(string response, APIContext context, APIRequest request); + } +} diff --git a/templates/csharp/codegen.mustache b/templates/csharp/codegen.mustache new file mode 100644 index 00000000..87bd59f9 --- /dev/null +++ b/templates/csharp/codegen.mustache @@ -0,0 +1,734 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to + * use, copy, modify, and distribute this software in source code or binary + * form for use in connection with the web services and APIs provided by + * Facebook. + * + * As with any software that integrates with the Facebook platform, your use + * of this software is subject to the Facebook Developer Principles and + * Policies [http://developers.facebook.com/policy/]. This copyright notice + * shall be included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using IDName = Facebook.Ads.SDK.IdName; + +namespace Facebook.Ads.SDK +{ + + sealed public class {{name:pascal_case}} : APINode + { + {{#fields}} + private {{{type:csharp}}} m{{name:pascal_case}}; + {{/fields}} + + {{#cls_is_ad_creative}} + private string mCreativeId; + {{/cls_is_ad_creative}} + + {{^has_get}}public {{/has_get}}{{#has_get}}{{#can_be_data_type}}public {{/can_be_data_type}}{{/has_get}}{{name:pascal_case}}() { + } + {{#has_get}} + + public {{name:pascal_case}}(long id, APIContext context) + : this(id.ToString(), context) + { + } + + public {{name:pascal_case}}(string id, APIContext context) + { + this.mId = id; + {{#cls_is_ad_creative}} + mCreativeId = mId.ToString(); + {{/cls_is_ad_creative}} + this.Context = context; + } + + public static {{name:pascal_case}} FetchById(long id, APIContext context) + { + return FetchById(id.ToString(), context); + } + + public static {{name:pascal_case}} FetchById(string id, APIContext context) + { + {{name:pascal_case}} {{name:camel_case}} = + new APIRequestGet(id, context) + .RequestAllFields() + .Execute() + .FirstOrDefault(); + return {{name:camel_case}}; + } + + private string PrefixedId + { + get + { + {{#cls_is_ad_account}} + return ("act_" + this.mId.ToString()); + {{/cls_is_ad_account}} + {{^cls_is_ad_account}} + return this.mId.ToString(); + {{/cls_is_ad_account}} + } + } + {{/has_get}} + {{#has_id}} + public string Id + { + get { return this.FieldId.ToString(); } + } + {{/has_id}} + + private static List ConvertToObjects(object input) + { + var objects = input as IEnumerable; + if (objects != null) + { + return new List(objects); + } + + var strings = input as IEnumerable; + if (strings != null) + { + return new List(strings); + } + return null; + } + + private static List ConvertToStrings(object input) + { + var objects = input as IEnumerable; + if (objects != null) + { + return objects.Select(obj => obj.ToString()).ToList(); + } + + var strings = input as IEnumerable; + if (strings != null) + { + return new List(strings); + } + return null; + } + + private static List ConvertToLongs(object input) + { + var objects = input as IEnumerable; + if (objects != null) + { + return objects.Select(obj => Convert.ToInt64(obj)).ToList(); + } + return null; + } + + private static List ConvertToDoubles(object input) + { + var objects = input as IEnumerable; + if (objects != null) + { + return objects.Select(obj => Convert.ToDouble(obj)).ToList(); + } + return null; + } + + private static List ConvertToBooleans(object input) + { + var objects = input as IEnumerable; + if (objects != null) + { + return objects.Select(obj => Convert.ToBoolean(obj)).ToList(); + } + return null; + } + + public static {{name:pascal_case}} LoadFromDictionary(dynamic dict) + { + if (!(dict is Dictionary)) { + return null; + } + {{name:pascal_case}} model = new {{name:pascal_case}}(); + model.Data = dict; + {{#fields}} + // BUGBUG: rf_spec has some issue of varying data type (can be string for map) + // We can't support it. + if (dict.ContainsKey("{{api_name}}") && !"rf_spec".Equals("{{api_name}}")) + { + {{#is_node}} + {{#is_list}} + // This field is a list of API Nodes. + var nodeList = dict["{{api_name}}"] as IEnumerable; + model.m{{name:pascal_case}} = nodeList + .Select(obj => + {{{csharp:node_base_type}}}.LoadFromDictionary(obj)) + .ToList(); + {{/is_list}} + {{^is_list}} + // This is an API node + model.m{{name:pascal_case}} = {{{type:csharp}}}.LoadFromDictionary(dict["{{api_name}}"]); + {{/is_list}} + {{/is_node}} + + {{^is_node}} + {{#is_list}} + {{#is_string_base_type}} + model.m{{{name:pascal_case}}} = ConvertToStrings(dict["{{{api_name}}}"]); + {{/is_string_base_type}} + {{#is_long_base_type}} + model.m{{{name:pascal_case}}} = ConvertToLongs(dict["{{{api_name}}}"]); + {{/is_long_base_type}} + {{#is_double_base_type}} + model.m{{{name:pascal_case}}} = ConvertToDoubles(dict["{{{api_name}}}"]); + {{/is_double_base_type}} + {{#is_bool_base_type}} + model.m{{{name:pascal_case}}} = ConvertToBooleans(dict["{{{api_name}}}"]); + {{/is_bool_base_type}} + {{#is_object_base_type}} + model.m{{{name:pascal_case}}} = ConvertToObjects(dict["{{{api_name}}}"]); + {{/is_object_base_type}} + {{/is_list}} + {{#is_map}} + // TODO: supporting map + {{/is_map}} + {{#is_enum}}{{^is_list}} + model.m{{{name:pascal_case}}} = {{{type:csharp}}}.CreateFromString((string)dict["{{{api_name}}}"]); + {{/is_list}}{{/is_enum}} + {{^is_list}}{{^is_map}}{{^is_enum}} + {{#is_long_base_type}} + model.m{{{name:pascal_case}}} = Convert.ToInt64(dict["{{{api_name}}}"]); + {{/is_long_base_type}} + {{#is_double_base_type}} + model.m{{{name:pascal_case}}} = Convert.ToDouble(dict["{{{api_name}}}"]); + {{/is_double_base_type}} + {{#is_bool_base_type}} + model.m{{{name:pascal_case}}} = Convert.ToBoolean(dict["{{{api_name}}}"]); + {{/is_bool_base_type}} + {{^is_long_base_type}}{{^is_double_base_type}}{{^is_bool_base_type}} + model.m{{{name:pascal_case}}} = dict["{{{api_name}}}"]; + {{/is_bool_base_type}}{{/is_double_base_type}}{{/is_long_base_type}} + {{/is_enum}}{{/is_map}}{{/is_list}} + {{/is_node}} + } + {{/fields}} + + return ({{name:pascal_case}})model; + } + + public static {{name:pascal_case}} LoadJSON(string json, APIContext context, bool isCollectionResponse) + { + dynamic obj = APINode.SerializeJSON(json); + return LoadFromDictionary(obj); + } + + public static APINodeList<{{name:pascal_case}}> ParseResponse(string json, APIContext context, APIRequest<{{name:pascal_case}}> request) + { + var modelList = new APINodeList<{{name:pascal_case}}>(request, json); + dynamic obj = APINode.SerializeJSON(json); + if (request.IsCollectionResponse) + { + dynamic list = obj["data"]; + foreach (var o in list) + { + modelList.Add(LoadFromDictionary(o)); + } + } + else + { + modelList.Add(LoadFromDictionary(obj)); + } + return modelList; + } + + public override string ToString() + { + // TODO: implement + return ""; + } + + #region API Requests + {{#apis}} + public APIRequest{{name:pascal_case}} {{name:camel_case}}() + { + return new APIRequest{{name:pascal_case}}(this.PrefixedId.ToString(), this.Context); + } + + {{/apis}} + #endregion + + #region Fields properties + {{#fields}} + public {{{type:csharp}}} Field{{name:pascal_case}} + { + get + { + {{#is_root_node}} + if (m{{name:pascal_case}} != null) + { + m{{name:pascal_case}}.Context = this.Context; + } + {{/is_root_node}} + return m{{name:pascal_case}}; + } + } + + {{^has_get}} + public {{cls:name:pascal_case}} setField{{name:pascal_case}}({{{type:csharp}}} value) { + this.m{{name:pascal_case}} = value; + return this; + } + + {{#is_node}} + public {{cls:name:pascal_case}} setField{{name:pascal_case}}(string value) { + // TODO: + // Type type = new TypeToken<{{{type:csharp}}}>(){}.getType(); + // this.m{{name:pascal_case}} = {{{csharp:node_base_type}}}.getGson().fromJson(value, type); + {{#cls_is_ad_creative}} + {{#is_id_field}} + // this.mCreativeId = this.mId.ToString(); + {{/is_id_field}} + {{/cls_is_ad_creative}} + return this; + } + {{/is_node}} + {{/has_get}} + + {{#has_get}} + {{#can_be_data_type}} + public {{cls:name:pascal_case}} setField{{name:pascal_case}}({{{type:csharp}}} value) { + // TODO: Implement + // this.m{{name:pascal_case}} = value; + {{#cls_is_ad_creative}} + {{#is_id_field}} + // this.mCreativeId = this.mId.ToString(); + {{/is_id_field}} + {{/cls_is_ad_creative}} + return this; + } + + {{#is_node}} + public {{cls:name:pascal_case}} setField{{name:pascal_case}}(string value) { + // TODO: Implement + // Type type = new TypeToken<{{{type:csharp}}}>(){}.getType(); + // this.m{{name:pascal_case}} = {{{csharp:node_base_type}}}.getGson().fromJson(value, type); + return this; + } + {{/is_node}} + {{/can_be_data_type}} + {{/has_get}} + {{/fields}} + + #endregion + + #region API Request inner classes + + {{#apis}} + {{#return_single_node}} + public class APIRequest{{name:pascal_case}} + : APIRequest<{{{return}}}{{^return}}APINode{{/return}}> + { + public static string[] PARAMS = + { + {{#params}} + "{{api_name}}", + {{/params}} + {{#allow_file_upload}} + "file", + {{/allow_file_upload}} + }; + + public static string[] FIELDS = + { + {{#param_fields}} + {{^not_visible}} + "{{api_name}}", + {{/not_visible}} + {{/param_fields}} + }; + + public APIRequest{{name:pascal_case}}(string nodeId, APIContext context) + : base(context, nodeId, "{{{endpoint}}}", "{{{method}}}", PARAMS) + { + } + + protected override APINodeList<{{{return}}}{{^return}}APINode{{/return}}> ParseResponse(string response) + { + return {{{return}}}{{^return}}APINode{{/return}}.ParseResponse(response, this.Context, this); + } + + public override bool IsCollectionResponse + { + get { return false; } + } + + public APIRequest{{api:name:pascal_case}} SetParam(string parameter, object value) + { + this.SetParamInternal(parameter, value); + return this; + } + + public APIRequest{{api:name:pascal_case}} SetParams(Dictionary parameters) + { + this.SetParamsInternal(parameters); + return this; + } + + {{#allow_file_upload}} + public APIRequest{{api:name:pascal_case}} AddUploadFile (string uploadName, FileInfo file) + { + this.SetParam(uploadName, file); + return this; + } + {{/allow_file_upload}} + + {{#params}} + public APIRequest{{api:name:pascal_case}} Set{{name:pascal_case}} ({{{type:csharp}}} value) + { + this.SetParam("{{api_name}}", value); + return this; + } + + {{^is_string}} + public APIRequest{{api:name:pascal_case}} Set{{name:pascal_case}} (string value) + { + this.SetParam("{{api_name}}", value); + return this; + } + {{/is_string}} + {{/params}} + + public APIRequest{{api:name:pascal_case}} RequestAllFields() + { + return this.RequestAllFields(true); + } + + public APIRequest{{api:name:pascal_case}} RequestAllFields(bool value) + { + foreach (string field in FIELDS) + { + this.RequestField(field, value); + } + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestFields(IEnumerable fields) + { + return this.RequestFields(fields, true); + } + + public APIRequest{{api:name:pascal_case}} RequestFields (IEnumerable fields, bool value) + { + foreach (string field in fields) + { + this.RequestField(field, value); + } + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestField (string field) + { + this.RequestField(field, true); + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestField (string field, bool value) + { + this.RequestFieldInternal(field, value); + return this; + } + + {{#param_fields}} + {{^not_visible}} + public APIRequest{{api:name:pascal_case}} Request{{name:pascal_case}}Field() + { + return this.Request{{name:pascal_case}}Field(true); + } + + public APIRequest{{api:name:pascal_case}} Request{{name:pascal_case}}Field(bool value) + { + this.RequestField("{{api_name}}", value); + return this; + } + + {{/not_visible}} + {{/param_fields}} + + {{#api_referenced_enum_types}} + public sealed class {{type:csharp}} { + private readonly string value; + + private static readonly Dictionary instance = + new Dictionary() + { + {{#values}} + {"{{csharp_name}}", new {{type:csharp}}("{{api_value}}")}, + {{/values}} + {"NULL", null}, + }; + + {{#values}} + public static readonly {{type:csharp}} VALUE_{{csharp_name}} = instance["{{csharp_name}}"]; + {{/values}} + + private {{type:csharp}}(string value) + { + this.value = value; + } + + public static {{type:csharp}} CreateFromString(string value) + { + if (!instance.ContainsKey(value)) + { + throw new ArgumentException(value + " is not a valid {{type:csharp}}", "value"); + } + return new {{type:csharp}}(value); + } + + public override string ToString() + { + return value; + } + } + {{/api_referenced_enum_types}} + } + + {{/return_single_node}} + {{^return_single_node}} + public class APIRequest{{name:pascal_case}} + : APIRequest<{{{return}}}{{^return}}APINode{{/return}}> + { + public static string[] PARAMS = + { + {{#params}} + "{{api_name}}", + {{/params}} + {{#allow_file_upload}} + "file", + {{/allow_file_upload}} + }; + + public static string[] FIELDS = + { + {{#param_fields}} + {{^not_visible}} + "{{api_name}}", + {{/not_visible}} + {{/param_fields}} + }; + + public APIRequest{{name:pascal_case}}(string nodeId, APIContext context) + : base(context, nodeId, "{{{endpoint}}}", "{{{method}}}", PARAMS) + { + } + + protected override APINodeList<{{{return}}}{{^return}}APINode{{/return}}> ParseResponse(string response) + { + return {{{return}}}{{^return}}APINode{{/return}}.ParseResponse(response, this.Context, this); + } + + public override bool IsCollectionResponse + { + get { return true; } + } + + public APIRequest{{api:name:pascal_case}} SetParam(string parameter, object value) + { + this.SetParamInternal(parameter, value); + return this; + } + + public APIRequest{{api:name:pascal_case}} SetParams(Dictionary parameters) + { + this.SetParamsInternal(parameters); + return this; + } + + {{#allow_file_upload}} + public APIRequest{{api:name:pascal_case}} AddUploadFile(string uploadName, FileInfo file) + { + this.SetParam(uploadName, file); + return this; + } + {{/allow_file_upload}} + + {{#params}} + public APIRequest{{api:name:pascal_case}} Set{{name:pascal_case}}({{{type:csharp}}} {{name:camel_case}}Value) + { + this.SetParam("{{api_name}}", {{name:camel_case}}Value); + return this; + } + + {{^is_string}} + public APIRequest{{api:name:pascal_case}} Set{{name:pascal_case}}(string {{name:camel_case}}Value) + { + this.SetParam("{{api_name}}", {{name:camel_case}}Value); + return this; + } + {{/is_string}} + {{/params}} + + public APIRequest{{api:name:pascal_case}} RequestAllFields() + { + return this.RequestAllFields(true); + } + + public APIRequest{{api:name:pascal_case}} RequestAllFields(bool value) + { + foreach (string field in FIELDS) + { + this.RequestField(field, value); + } + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestFields(IEnumerable fields) + { + return this.RequestFields(fields, true); + } + + public APIRequest{{api:name:pascal_case}} RequestFields(IEnumerable fields, bool value) + { + foreach (string field in fields) + { + this.RequestField(field, value); + } + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestField(string field) + { + this.RequestField(field, true); + return this; + } + + public APIRequest{{api:name:pascal_case}} RequestField(string field, bool value) + { + this.RequestFieldInternal(field, value); + return this; + } + + {{#param_fields}} + {{^not_visible}} + public APIRequest{{api:name:pascal_case}} Request{{name:pascal_case}}Field() + { + return this.Request{{name:pascal_case}}Field(true); + } + + public APIRequest{{api:name:pascal_case}} Request{{name:pascal_case}}Field(bool value) + { + this.RequestField("{{api_name}}", value); + return this; + } + + {{/not_visible}} + {{/param_fields}} + + {{#api_referenced_enum_types}} + public sealed class {{type:csharp}} { + private readonly string value; + + private static readonly Dictionary instance = + new Dictionary() + { + {{#values}} + {"{{csharp_name}}", new {{type:csharp}}("{{api_value}}")}, + {{/values}} + {"NULL", null}, + }; + + {{#values}} + public static readonly {{type:csharp}} VALUE_{{csharp_name}} = instance["{{csharp_name}}"]; + {{/values}} + + private {{type:csharp}}(string value) { + this.value = value; + } + + public override string ToString() { + return value; + } + } + {{/api_referenced_enum_types}} + } + + {{/return_single_node}} + {{/apis}} + #endregion + + #region Referenced Enum types + + {{#cls_referenced_enum_types}} + public sealed class {{type:csharp}} + { + private readonly string value; + + private static readonly Dictionary instance = + new Dictionary() + { + {{#values}} + {"{{csharp_name}}", new {{type:csharp}}("{{api_value}}")}, + {{/values}} + {"NULL", null}, + }; + + {{#values}} + public static readonly {{type:csharp}} VALUE_{{csharp_name}} = instance["{{csharp_name}}"]; + {{/values}} + + private {{type:csharp}}(string value) + { + this.value = value; + } + + public static {{type:csharp}} CreateFromString(string value) + { + if (!instance.ContainsKey(value)) + { + throw new ArgumentException(value + " is not a valid {{type:csharp}}", "value"); + } + return new {{type:csharp}}(value); + } + + public override string ToString() + { + return value; + } + } + {{/cls_referenced_enum_types}} + + #endregion + + public {{name:pascal_case}} CopyFrom({{name:pascal_case}} instance) + { + // TODO: implement + return this; + } + + public static IResponseParser<{{name:pascal_case}}> GetParser() + { + return new {{name:pascal_case}}ResponseParser(); + } + + class {{name:pascal_case}}ResponseParser : IResponseParser<{{name:pascal_case}}> + { + public APINodeList<{{name:pascal_case}}> ParseResponse(string response, APIContext context, APIRequest<{{name:pascal_case}}> request) + { + return {{name:pascal_case}}.ParseResponse(response, context, request); + } + } + } +}