Submit and vote on feature ideas.

Welcome to the new Parasoft forums! We hope you will enjoy the site and try out some of the new features, like sharing an idea you may have for one of our products or following a category.

GraphQL SDL tips

Options
benken_parasoft
benken_parasoft Posts: 1,230 ✭✭✭

The Virtualize Message Responder has a Form JSON view that can be constrained to the JSON Schema types defined in a service definition document. This simplifies configuring the response, where the Form JSON view is populated with controls to select and configure the available JSON properties, array items, etc.

What if you are virtualizing a GraphQL endpoint that is not described by a service definition document such as an OpenAPI? Fortunately, it is possible to generate JSON Schema types from a GraphQL Schema Definition Language (SDL) document and reference them from a service definition.

In a provisioning action, create an Extension Tool with the following Groovy script:

import java.io.IOException
import java.io.StringWriter
import java.util.List

import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonGenerator
import com.parasoft.api.IOUtil
import com.parasoft.api.ScriptingContext

import graphql.Scalars
import graphql.introspection.Introspection
import graphql.schema.GraphQLEnumType
import graphql.schema.GraphQLEnumValueDefinition
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLImplementingType
import graphql.schema.GraphQLInterfaceType
import graphql.schema.GraphQLNamedOutputType
import graphql.schema.GraphQLNamedSchemaElement
import graphql.schema.GraphQLNamedType
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLOutputType
import graphql.schema.GraphQLScalarType
import graphql.schema.GraphQLSchema
import graphql.schema.GraphQLTypeUtil
import graphql.schema.GraphQLUnionType
import graphql.schema.idl.SchemaGenerator

String convertToJsonSchema(def input, def context) throws IOException {
    def sdlContent = IOUtil.readTextFile(context.getAbsolutePathFile(input.toString()))
    GraphQLSchema graphQLSchema = SchemaGenerator.createdMockedSchema(sdlContent)
    String jsonSchema = dumpSchema(graphQLSchema)
    return jsonSchema
}

static String dumpSchema(GraphQLSchema schema) throws IOException {
    StringWriter sw = new StringWriter()
    JsonFactory jsonFactory = new JsonFactory()
    jsonFactory.createGenerator(sw).withCloseable { generator ->
        generator.useDefaultPrettyPrinter()
        generator.writeStartObject()
        generator.writeFieldName('$ref')
        generator.writeString('#/' + GraphQLSchema.class.getSimpleName())
        writeRootType(schema, generator)
        for (GraphQLNamedSchemaElement type : schema.getAllElementsAsList()) {
            if (type instanceof GraphQLNamedType && Introspection.isIntrospectionTypes((GraphQLNamedType) type)) {
                continue
            }
            if (type instanceof GraphQLImplementingType) {
                dumpGraphQLImplementingType((GraphQLImplementingType) type, generator)
            } else if (type instanceof GraphQLUnionType) {
                dumpGraphQLUnionType((GraphQLUnionType) type, generator)
            } else if (type instanceof GraphQLEnumType) {
                dumpGraphQLEnumType((GraphQLEnumType) type, generator)
            }
        }
        generator.writeEndObject()
    }
    return sw.toString()
}

static void writeRootType(GraphQLSchema schema, JsonGenerator generator) throws IOException {
    generator.writeFieldName(GraphQLSchema.class.getSimpleName())
    generator.writeStartObject()
    generator.writeFieldName('oneOf')
    generator.writeStartArray()
    writeRootOperationType(schema.getQueryType(), generator)
    writeRootOperationType(schema.getMutationType(), generator)
    writeRootOperationType(schema.getSubscriptionType(), generator)
    generator.writeEndArray()
    generator.writeEndObject()
}

static void writeRootOperationType(GraphQLObjectType type, JsonGenerator generator) throws IOException {
    if (type != null) {
        writeRef(type, generator)
    }
}

static void startGraphQLNamedSchemaElement(GraphQLNamedSchemaElement type, JsonGenerator generator) throws IOException {
    generator.writeFieldName(type.getName())
    generator.writeStartObject()
    String description = type.getDescription()
    if (description != null && !description.isBlank()) {
        generator.writeFieldName('description')
        generator.writeString(description)
    }
}

static void dumpGraphQLEnumType(GraphQLEnumType type, JsonGenerator generator) throws IOException {
    startGraphQLNamedSchemaElement(type, generator)
    generator.writeFieldName('enum')
    generator.writeStartArray()
    for (GraphQLEnumValueDefinition enumValue : type.getValues()) {
        generator.writeString(enumValue.getName())
    }
    generator.writeEndArray()
    generator.writeEndObject()
}

static void dumpGraphQLUnionType(GraphQLUnionType type, JsonGenerator generator) throws IOException {
    startGraphQLNamedSchemaElement(type, generator)
    List<GraphQLNamedOutputType> members = type.getTypes()
    if (!members.isEmpty()) {
        generator.writeFieldName('oneOf')
        generator.writeStartArray()
        for (GraphQLNamedOutputType member : members) {
            writeRef(member, generator)
        }
        generator.writeEndArray()
    }
    generator.writeEndObject()
}

static void dumpGraphQLImplementingType(GraphQLImplementingType type, JsonGenerator generator) throws IOException {
    startGraphQLNamedSchemaElement(type, generator)
    List<GraphQLNamedOutputType> interfaces = type.getInterfaces()
    if (!interfaces.isEmpty()) {
        generator.writeFieldName('allOf')
        generator.writeStartArray()
        for (GraphQLNamedOutputType anInterface : interfaces) {
            writeRef(anInterface, generator)
        }
        generator.writeStartObject()
        writeProperties(type, interfaces, generator)
        generator.writeEndObject()
        generator.writeEndArray()
    } else {
        writeProperties(type, interfaces, generator)
    }
    generator.writeEndObject()
}

static void writeProperties(GraphQLImplementingType type, List<GraphQLNamedOutputType> interfaces, JsonGenerator generator) throws IOException {
    generator.writeFieldName('type')
    generator.writeString('object')
    List<GraphQLFieldDefinition> fieldDefinitions = type.getFieldDefinitions()
    if (!fieldDefinitions.isEmpty()) {
        generator.writeFieldName('properties')
        generator.writeStartObject()
        for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) {
            if (!inheritedField(fieldDefinition, interfaces)) {
                generator.writeFieldName(fieldDefinition.getName())
                dumpGraphQLOutputType(fieldDefinition.getType(), generator)
            }
        }
        generator.writeEndObject()
    }
}

static boolean inheritedField(GraphQLFieldDefinition fieldDefinition, List<GraphQLNamedOutputType> interfaces) {
    for (GraphQLNamedOutputType type : interfaces) {
        if (type instanceof GraphQLInterfaceType) {
            GraphQLInterfaceType anInterface = (GraphQLInterfaceType) type
            if (anInterface.getFieldDefinition(fieldDefinition.getName()) != null ||
                    inheritedField(fieldDefinition, anInterface.getInterfaces())) {
                return true
            }
        }
    }
    return false
}

static void dumpGraphQLOutputType(GraphQLOutputType type, JsonGenerator generator) throws IOException {
    if (GraphQLTypeUtil.isNonNull(type)) {
        dumpGraphQLOutputTypeNullable(GraphQLTypeUtil.unwrapOneAs(type), generator)
    } else {
        generator.writeStartObject()
        generator.writeFieldName('oneOf')
        generator.writeStartArray()
        dumpGraphQLOutputTypeNullable(type, generator)
        generator.writeStartObject()
        generator.writeFieldName('type')
        generator.writeString('null')
        generator.writeEndObject()
        generator.writeEndArray()
        generator.writeEndObject()
    }
}

static void dumpGraphQLOutputTypeNullable(GraphQLOutputType type, JsonGenerator generator) throws IOException {
    if (GraphQLTypeUtil.isList(type)) {
        generator.writeStartObject()
        generator.writeFieldName('type')
        generator.writeString('array')
        generator.writeFieldName('items')
        dumpGraphQLOutputType(GraphQLTypeUtil.unwrapOneAs(type), generator)
        generator.writeEndObject()
    } else if (type instanceof GraphQLScalarType) {
        dumpGraphQLScalarType((GraphQLScalarType) type, generator)
    } else if (type instanceof GraphQLNamedOutputType) {
        writeRef((GraphQLNamedOutputType) type, generator)
    } else {
        generator.writeStartObject()
        generator.writeEndObject()
    }
}

static void dumpGraphQLScalarType(GraphQLScalarType type, JsonGenerator generator) throws IOException {
    generator.writeStartObject()
    generator.writeFieldName('type')
    String jsonType = null
    String format = null
    if (Scalars.GraphQLBoolean.equals(type)) {
        jsonType = 'boolean'
    } else if (Scalars.GraphQLFloat.equals(type)) {
        jsonType = 'number'
        format = 'float'
    } else if (Scalars.GraphQLID.equals(type)) {
        jsonType = 'string'
    } else if (Scalars.GraphQLInt.equals(type)) {
        jsonType = 'integer'
        format = 'int32'
    } else if (Scalars.GraphQLString.equals(type)) {
        jsonType = 'string'
    } else {
        switch (type.getName()) {
        case 'BigInt':
            jsonType = 'integer'
            break
        case 'Byte':
            jsonType = 'integer'
            format = 'int8'
            break
        case 'Date':
            jsonType = 'string'
            format = 'date'
            break
        case 'DateTime':
            jsonType = 'string'
            format = 'date-time'
            break
        case 'Duration':
            jsonType = 'string'
            format = 'duration'
            break
        case 'EmailAddress':
            jsonType = 'string'
            format = 'email'
            break
        case 'IPv4':
            jsonType = 'string'
            format = 'ipv4'
            break
        case 'IPv6':
            jsonType = 'string'
            format = 'ipv6'
            break
        case 'LocalDate':
            jsonType = 'string'
            format = 'date'
            break
        case 'LocalEndTime':
            jsonType = 'string'
            format = 'time'
            break
        case 'LocalTime':
            jsonType = 'string'
            format = 'time'
            break
        case 'NegativeFloat':
            jsonType = 'number'
            break
        case 'NegativeInt':
            jsonType = 'integer'
            break
        case 'NonNegativeFloat':
            jsonType = 'number'
            break
        case 'NonNegativeInt':
            jsonType = 'integer'
            break
        case 'NonPositiveFloat':
            jsonType = 'number'
            break
        case 'NonPositiveInt':
            jsonType = 'integer'
            break
        case 'Port':
            jsonType = 'integer'
            format = 'int16'
            break
        case 'PositiveFloat':
            jsonType = 'number'
            break
        case 'PositiveInt':
            jsonType = 'integer'
            break
        case 'RegularExpression':
            jsonType = 'string'
            format = 'regex'
            break
        case 'SafeInt':
            jsonType = 'integer'
            break
        case 'Time':
            jsonType = 'string'
            format = 'time'
            break
        case 'Timestamp':
            jsonType = 'integer'
            format = 'int64'
            break
        case 'URL':
            jsonType = 'string'
            format = 'uri'
            break
        case 'UUID':
            jsonType = 'string'
            format = 'uuid'
            break
        default:
            jsonType = 'string'
            break
        }
    }
    generator.writeString(jsonType)
    if (format != null) {
        generator.writeFieldName('format')
        generator.writeString(format)
    }
    generator.writeEndObject()
}


static void writeRef(GraphQLNamedOutputType type, JsonGenerator generator) throws IOException {
    generator.writeStartObject()
    generator.writeFieldName('$ref')
    generator.writeString('#/' + type.getName())
    generator.writeEndObject()
}

In the Method box, choose "convertToJsonSchema". On the Input tab, click File then provide the path to your GraphQL SDL document.

Next, chain a Write File tool to the Extension Tool's "Return Value" output. In the "Target name" field, type a file name for your JSON Schema document (schema.json). In the "Target directory" field, type "." to save the file in the same directory as your provisioning action.

Next, create a contrived service definition document to reference the JSON Schema document.

RAML example:

#%RAML 1.0
title: GraphQL
/graphql:
  get:
    responses:
      200:
        body:
          application/json:
            type: !include schema.json

OpenAPI example:

openapi: 3.0.0
info:
  version: 1.0.0
  title: GraphQL
paths:
  /graphql:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: "schema.json#/GraphQLSchema"

In a virtual asset, create a Message Responder. On the Definition tab, configure the URL of your contrived service definition. The Response tab will have the Form JSON view automatically configured based on the JSON Schema.

Comments

  • benken_parasoft
    benken_parasoft Posts: 1,230 ✭✭✭
    Options

    What if you do not have access to the GraphQL SDL document? What if it is difficult to obtain? If the GraphQL endpoint has introspection enabled then you can generate a GraphQL SDL document from an introspection result.

    See GraphQL SDL tips on the SOAtest forum for detail.