Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 49de748

Browse files
committed
Feature: convert graphql query to avro schema
Extend query object: add `avro_schema` method, which produces Avro schema which can be used to verify or flatten any `query_exequte` result. Closes #7
1 parent 689df3a commit 49de748

File tree

5 files changed

+408
-1
lines changed

5 files changed

+408
-1
lines changed

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ default:
33

44
.PHONY: lint
55
lint:
6-
luacheck graphql/*.lua test/local/*.lua test/testdata/*.lua \
6+
luacheck graphql/*.lua \
7+
test/local/*.lua \
8+
test/testdata/*.lua \
79
test/common/*.test.lua test/common/lua/*.lua \
10+
test/extra/*.test.lua \
811
--no-redefined --no-unused-args
912

1013
.PHONY: test

graphql/query_to_avro.lua

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
--- Module for convertion GraphQL query to Avro schema.
2+
---
3+
--- Random notes:
4+
---
5+
--- * The best way to use this module is to just call `avro_schema` methon on
6+
--- compiled query object.
7+
local path = "graphql.core"
8+
local introspection = require(path .. '.introspection')
9+
local query_util = require(path .. '.query_util')
10+
11+
-- module functions
12+
local query_to_avro = {}
13+
14+
local gql_scalar_to_avro_index = {
15+
String = "string",
16+
Int = "int",
17+
Long = "long",
18+
-- GraphQL Float is double precision according to graphql.org.
19+
-- More info http://graphql.org/learn/schema/#scalar-types
20+
Float = "double",
21+
Boolean = "boolean"
22+
}
23+
local function gql_scalar_to_avro(fieldType)
24+
assert(fieldType.__type == "Scalar", "GraphQL scalar field expected")
25+
local result = gql_scalar_to_avro_index[fieldType.name]
26+
assert(result ~= nil, "Unexpected scalar type: " .. fieldType.name)
27+
return result
28+
end
29+
30+
local object_to_avro
31+
32+
local function complete_field_to_avro(fieldType, result, subSelections, context)
33+
local fieldTypeName = fieldType.__type
34+
35+
if fieldTypeName == 'List' then
36+
local innerType = fieldType.ofType
37+
local items = complete_field_to_avro(innerType, {}, subSelections,
38+
context).type
39+
result.type = {
40+
type = "array",
41+
items = items
42+
}
43+
return result
44+
end
45+
46+
if fieldTypeName == 'Object' then
47+
result.type = object_to_avro(fieldType, subSelections, context)
48+
return result
49+
elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then
50+
error('Interfases and Utions are not supported yet')
51+
end
52+
error(string.format('Unknown type "%s"', fieldTypeName))
53+
end
54+
55+
--- The function converts a single Object field to avro format
56+
local function field_to_avro(object_type, fields, context)
57+
local firstField = fields[1]
58+
assert(#fields == 1, "The aliases are not considered yet")
59+
local fieldName = firstField.name.value
60+
local fieldType = introspection.fieldMap[fieldName] or
61+
object_type.fields[fieldName]
62+
assert(fieldType ~= nil)
63+
local subSelections = query_util.mergeSelectionSets(fields)
64+
local innerType = fieldType.kind
65+
local fieldTypeName = innerType.__type
66+
local nullable = true
67+
-- In case the field is NonNull, the real type is in ofType attibute.
68+
if fieldTypeName == 'NonNull' then
69+
innerType = innerType.ofType
70+
nullable = false
71+
end
72+
local result = {}
73+
result.name = fieldName
74+
if fieldType.resolve ~= nil then
75+
-- not scalar
76+
return complete_field_to_avro(innerType, result, subSelections, context)
77+
end
78+
-- scalar type
79+
result.type = gql_scalar_to_avro(innerType)
80+
result.type = nullable and result.type .. "*" or result.type
81+
return result
82+
end
83+
84+
--- Convert GraphQL object to avro record.
85+
---
86+
--- @tparam table object_type GraphQL type object to be converted to Avro schema
87+
---
88+
--- @tparam table selections GraphQL representations of fields which should be
89+
--- in the output of the query
90+
---
91+
--- @tparam table context additional information for Avro schema generation; one
92+
--- of the fields is `namespace_parts` -- table of names of records from the
93+
--- root to the current object
94+
---
95+
--- @treturn table corresponding Avro schema
96+
object_to_avro = function(object_type, selections, context)
97+
local groupedFieldSet = query_util.collectFields(object_type, selections,
98+
{}, {}, context)
99+
local result = {
100+
type = 'record',
101+
name = object_type.name,
102+
fields = {}
103+
}
104+
if #context.namespace_parts ~= 0 then
105+
result.namespace = table.concat(context.namespace_parts, ".")
106+
end
107+
table.insert(context.namespace_parts, result.name)
108+
for _, fields in pairs(groupedFieldSet) do
109+
local avro_field = field_to_avro(object_type, fields, context)
110+
table.insert(result.fields, avro_field)
111+
end
112+
context.namespace_parts[#context.namespace_parts] = nil
113+
return result
114+
end
115+
116+
--- Create an Avro schema for a given query.
117+
---
118+
--- @tparam table query object which avro schema should be created for
119+
---
120+
--- @treturn table `avro_schema` avro schema for any `query:execute()` result.
121+
function query_to_avro.convert(query)
122+
local state = query.state
123+
local context = query_util.buildContext(state.schema, query.ast, {}, {},
124+
query.operation_name)
125+
-- The variable is necessary to avoid fullname interferention.
126+
-- Each nested Avro record creates it's namespace.
127+
context.namespace_parts = {}
128+
local rootType = state.schema[context.operation.operation]
129+
local selections = context.operation.selectionSet.selections
130+
return object_to_avro(rootType, selections, context)
131+
end
132+
133+
return query_to_avro

graphql/tarantool_graphql.lua

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local schema = require('graphql.core.schema')
1414
local types = require('graphql.core.types')
1515
local validate = require('graphql.core.validate')
1616
local execute = require('graphql.core.execute')
17+
local query_to_avro = require('graphql.query_to_avro')
1718

1819
local utils = require('graphql.utils')
1920

@@ -595,6 +596,7 @@ local function gql_compile(state, query)
595596
local gql_query = setmetatable(qstate, {
596597
__index = {
597598
execute = gql_execute,
599+
avro_schema = query_to_avro.convert
598600
}
599601
})
600602
return gql_query

test/extra/suite.ini

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[default]
2+
core = app
3+
description = tests on features which are not related to specific executor
4+
lua_libs = ../common/lua/test_data_user_order.lua

0 commit comments

Comments
 (0)