"use strict";
/*
 * Copyright 2018-2019 Unbounded Systems, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const graphql_1 = require("graphql");
const utils_1 = require("@adpt/utils");
const graphql_type_json_1 = tslib_1.__importDefault(require("graphql-type-json"));
const ld = tslib_1.__importStar(require("lodash"));
const util = tslib_1.__importStar(require("util"));
const swagger_types_1 = require("../../src/swagger2gql/swagger_types");
const type_resolver_1 = require("../../src/swagger2gql/type_resolver");
function isComplexType(schema) {
    return schema.type === "object" || schema.type === "array";
}
function buildFieldsFromSchema(baseName, schema, tyResolver, inputType) {
    const properties = schema.properties;
    //FIXME(manishv) add a unit test for these two cases below, do not rely on k8s tests
    if (properties === undefined)
        return graphql_type_json_1.default;
    if (Object.keys(properties).length === 0)
        return graphql_type_json_1.default;
    return () => {
        const ret = {};
        const required = schema.required ? schema.required : [];
        for (const propName in properties) {
            if (propName.startsWith("x-"))
                continue; // Do not try to process custom extensions
            if (!Object.hasOwnProperty.call(properties, propName))
                continue;
            const prop = properties[propName];
            const nonNull = required.find((val) => val === propName) !== undefined;
            const baseType = getOrBuildType(baseName + "_" + propName, prop, tyResolver, inputType);
            const type = nonNull ? new graphql_1.GraphQLNonNull(baseType) : baseType;
            ret[makeGQLFieldName(propName)] = {
                description: schema.description,
                type
            };
        }
        if (Object.keys(ret).length === 0) {
            throw new Error("Internal Error: no fields, how'd we get here??");
        }
        return ret;
    };
}
function makeGQLTypeName(swaggerName) {
    //FIXME(manishv) make robust removing all legal swagger characters that are illegal in graphql
    let gqlName = swaggerName.replace("#/definitions/", "");
    gqlName = gqlName.replace(/[\/\.-]/g, "_");
    return gqlName;
}
function makeGQLFieldName(swaggerName) {
    //FIXME(manishv) make robust removing all legal swagger characters that are illegal in graphql
    const gqlName = swaggerName.replace(/\$/, "dollar_");
    return gqlName;
}
function jsonSchema2GraphQLType(name, schema, tyResolverIn, inputType) {
    const tyResolver = inputType ? tyResolverIn.input : tyResolverIn.output;
    if (schema.type === "array") {
        const items = schema.items;
        if (!items)
            throw new Error(`Saw array schema with no items: ${util.inspect(schema)}`);
        const itemsName = `${name}_items`;
        if (swagger_types_1.isRef(items) || !isComplexType(items)) {
            return new graphql_1.GraphQLList(getOrBuildType(itemsName, items, tyResolverIn, inputType));
        }
        return new graphql_1.GraphQLList(jsonSchema2GraphQLType(itemsName, items, tyResolverIn, inputType));
    }
    const primitive = isPrimitive(schema.type) ? tyResolver.getType(schema.type) : undefined;
    if (primitive)
        return primitive;
    const gqlName = makeGQLTypeName(name) + (inputType ? "_input" : "_output");
    const consArgs = {
        name: gqlName,
        description: schema.description,
    };
    if (inputType) {
        const fields = buildFieldsFromSchema(name, schema, tyResolverIn, true);
        if (!ld.isFunction(fields))
            return fields;
        return new graphql_1.GraphQLInputObjectType(Object.assign({}, consArgs, { fields }));
    }
    else {
        //FIXME(manishv) attach resolvers somewhere
        const fields = buildFieldsFromSchema(name, schema, tyResolverIn, false);
        if (!ld.isFunction(fields))
            return fields;
        return new graphql_1.GraphQLObjectType(Object.assign({}, consArgs, { fields }));
    }
}
function getDef(schema, tyName) {
    if (!tyName.startsWith("#/definitions/"))
        return;
    const defs = schema.definitions;
    if (!defs)
        return;
    const baseTyName = tyName.replace("#/definitions/", "");
    return defs[baseTyName];
}
function resolveParam(schema, tyName) {
    const params = schema.parameters;
    if (!params || !tyName.startsWith("#/parameters/")) {
        throw new Error(`Unable to look up parameter name ${tyName}`);
    }
    const baseTyName = tyName.replace("#/parameters/", "");
    const param = params[baseTyName];
    if (!param)
        throw new Error(`Parameter name ${tyName} not found`);
    return param;
}
function resolveType(swagger, tyName, tyResolver, input) {
    const schema = getDef(swagger, tyName);
    if (schema === undefined)
        throw new Error(`Unable to find type '${tyName}'`);
    return jsonSchema2GraphQLType(tyName, schema, tyResolver, input);
}
const primitiveTypes = {
    integer: graphql_1.GraphQLInt,
    number: graphql_1.GraphQLFloat,
    string: graphql_1.GraphQLString,
    boolean: graphql_1.GraphQLBoolean,
    _Empty: {
        input: graphql_type_json_1.default,
        output: graphql_type_json_1.default
    }
};
function isPrimitive(tyName) {
    return Object.hasOwnProperty.call(primitiveTypes, tyName);
}
function populateBasicSwaggerTypes(tyResolver) {
    for (const nameI in primitiveTypes) {
        if (!Object.hasOwnProperty.call(primitiveTypes, nameI))
            continue;
        const name = nameI;
        const typ = primitiveTypes[name];
        const inTyp = "input" in typ ? typ.input : typ;
        const outTyp = "output" in typ ? typ.output : typ;
        tyResolver.input.addType(name, inTyp);
        tyResolver.output.addType(name, outTyp);
    }
}
function getOrBuildType(typeName, refOrSchema, tyResolver, input) {
    const name = swagger_types_1.isRef(refOrSchema) ? refOrSchema.$ref : typeName;
    try {
        const type = input ?
            tyResolver.input.getType(name) :
            tyResolver.output.getType(name);
        if (type)
            return type;
    }
    catch (err) {
        if (!ld.isError(err) || !err.message.startsWith("Unable to find type")) {
            throw err;
        }
    }
    if (swagger_types_1.isRef(refOrSchema))
        throw new Error(`Unable to find ref type ${name}`);
    return jsonSchema2GraphQLType(typeName, refOrSchema, tyResolver, input);
}
function getParameterInfo(operationId, param, tyResolver) {
    let type;
    if (param.in === "body") {
        return {
            type: getOrBuildType(`${operationId}_body`, param.schema, tyResolver, true),
            required: param.required ? param.required : false,
        };
    }
    if (param.type === "array") {
        const items = param.items;
        if (!items)
            throw new Error(`Saw array param with no items: ${util.inspect(param)}`);
        type = new graphql_1.GraphQLList(getOrBuildType(`${operationId}_${param.name}`, items, tyResolver, true));
    }
    else {
        type = tyResolver.input.getType(param.type);
    }
    return {
        type,
        required: param.required ? param.required : false,
        default: param.default
    };
}
function buildArgsForOperation(operationId, parameters, tyResolver) {
    if (parameters.length === 0)
        return;
    if (operationId == null)
        throw new Error(`operationId is null`);
    const ret = {};
    for (let param of parameters) {
        if (swagger_types_1.isRef(param)) {
            param = tyResolver.param.getType(param.$ref);
        }
        const info = getParameterInfo(operationId, param, tyResolver);
        const defaultValue = param.in === "body" ? undefined : param.default;
        ret[makeGQLTypeName(param.name)] = {
            type: (info.required ? new graphql_1.GraphQLNonNull(info.type) : info.type),
            defaultValue
        };
    }
    return ret;
}
function responseTypeForOperation(op, tyResolver) {
    const okResponse = op.responses["200"]; //FIXME(manishv) deal with other non-error responses here
    if (okResponse === undefined)
        return tyResolver.output.getType("_Empty");
    const schema = okResponse.schema;
    if (schema === undefined)
        return graphql_type_json_1.default;
    return getOrBuildType(op.operationId + "_Response", schema, tyResolver, false);
}
function buildQueryField(op, itemParams, tyResolver) {
    const iparams = itemParams ? itemParams : [];
    const oparams = op.parameters ? op.parameters : [];
    return {
        type: responseTypeForOperation(op, tyResolver),
        args: buildArgsForOperation(op.operationId, [...iparams, ...oparams], tyResolver)
    };
}
const supportedOps = utils_1.tuple("get", "post");
function buildQueryFields(swagger, tyResolver) {
    const ret = {};
    for (const swagPath in swagger.paths) {
        if (!Object.hasOwnProperty.call(swagger.paths, swagPath))
            continue;
        const pathItem = swagger.paths[swagPath];
        for (const opType of supportedOps) {
            const op = pathItem[opType];
            if (op === undefined)
                continue;
            if (op.operationId === undefined)
                continue; //FIXME(manishv) should we compute a name here?
            ret[makeGQLFieldName(op.operationId)] = buildQueryField(op, pathItem.parameters, tyResolver);
        }
    }
    return ret;
}
function buildQueryObject(swagger) {
    const tyResolver = {
        input: new type_resolver_1.TypeResolver((n) => resolveType(swagger, n, tyResolver, true)),
        output: new type_resolver_1.TypeResolver((n) => resolveType(swagger, n, tyResolver, false)),
        param: new type_resolver_1.TypeResolver((n) => resolveParam(swagger, n)),
    };
    populateBasicSwaggerTypes(tyResolver);
    const fields = buildQueryFields(swagger, tyResolver);
    return new graphql_1.GraphQLObjectType({
        name: "Query",
        fields
    });
}
function addResolversToFields(seen, obj, getResolver, isQuery = false) {
    if (seen.has(obj))
        return;
    seen.add(obj);
    const fields = obj.getFields();
    for (const fieldName in fields) {
        if (!Object.hasOwnProperty.call(fields, fieldName))
            continue;
        const field = fields[fieldName];
        const fieldResolvers = getResolver.fieldResolvers;
        const resolver = fieldResolvers ? fieldResolvers(obj, fieldName, isQuery) : undefined;
        field.resolve = resolver;
        const fieldType = field.type;
        if (graphql_1.isObjectType(fieldType)) {
            addResolversToFields(seen, fieldType, getResolver, false);
        }
        //FIXME(manishv) How to deal with custom scalar types?
    }
}
function addResolversToSchema(schema, getResolver) {
    const queryType = schema.getQueryType();
    if (!queryType)
        return;
    addResolversToFields(new Set(), queryType, getResolver, true);
    return;
}
function buildGraphQLSchema(swagger, getResolver) {
    const qobj = buildQueryObject(swagger);
    const schema = new graphql_1.GraphQLSchema({ query: qobj });
    if (getResolver)
        addResolversToSchema(schema, getResolver);
    return schema;
}
exports.buildGraphQLSchema = buildGraphQLSchema;
//# sourceMappingURL=converter.js.map