Bläddra i källkod

Make validate not suck; Add rules;

bjorn 9 år sedan
förälder
incheckning
b0b82bd5d3
2 ändrade filer med 203 tillägg och 65 borttagningar
  1. 108 0
      rules.lua
  2. 95 65
      validate.lua

+ 108 - 0
rules.lua

@@ -0,0 +1,108 @@
+local rules = {}
+
+function rules.uniqueOperationNames(node, context)
+  local name = node.name and node.name.value
+
+  if name then
+    if context.operationNames[name] then
+      error('Multiple operations exist named "' .. name .. '"')
+    end
+
+    context.operationNames[name] = true
+  end
+end
+
+function rules.loneAnonymousOperation(node, context)
+  local name = node.name and node.name.value
+
+  if context.hasAnonymousOperation or (not name and next(context.operationNames)) then
+    error('Cannot have more than one operation when using anonymous operations')
+  end
+
+  if not name then
+    context.hasAnonymousOperation = true
+  end
+end
+
+function rules.fieldsDefinedOnType(node, context)
+  if context.currentField == false then
+    local parent = context.objects[#context.objects - 1]
+    error('Field "' .. node.name.value .. '" is not defined on type "' .. parent.name .. '"')
+  end
+end
+
+function rules.argumentsDefinedOnType(node, context)
+  if node.arguments then
+    for _, argument in pairs(node.arguments) do
+      local name = argument.name.value
+      if not context.currentField.arguments[name] then
+        error('Non-existent argument "' .. name .. '"')
+      end
+    end
+  end
+end
+
+function rules.scalarFieldsAreLeaves(node, context)
+  if context.currentField.__type == 'Scalar' and node.selectionSet then
+    error('Scalar values cannot have subselections')
+  end
+end
+
+function rules.compositeFieldsAreNotLeaves(node, context)
+  local _type = context.currentField.__type
+  local isCompositeType = _type == 'Object' or _type == 'Interface' or _type == 'Union'
+
+  if isCompositeType and not node.selectionSet then
+    error('Composite types must have subselections')
+  end
+end
+
+function rules.inlineFragmentValidTypeCondition(node, context)
+  if not node.typeCondition then return end
+
+  local kind = context.objects[#context.objects]
+
+  if kind == false then
+    error('Inline fragment type condition refers to non-existent type')
+  end
+
+  if kind.__type ~= 'Object' and kind.__type ~= 'Interface' and kind.__type ~= 'Union' then
+    error('Inline fragment type condition was not an Object, Interface, or Union')
+  end
+end
+
+function rules.unambiguousSelections(node, context)
+  local selectionMap = {}
+
+  -- FIXME
+  local function canMerge(fieldA, fieldB)
+    return fieldA.__type == fieldB.__type and fieldA.name == fieldB.name
+  end
+
+  local function validateSelection(key, kind)
+    if selectionMap[key] and not canMerge(selectionMap[key], kind) then
+      error('Type mismatch')
+    end
+
+    selectionMap[key] = kind
+  end
+
+  -- Recursively make sure that there are no ambiguous selections with the same name.
+  local function validateSelectionSet(selectionSet)
+    for _, selection in ipairs(selectionSet.selections) do
+      if selection.kind == 'field' then
+        local selectionKey = selection.alias and selection.alias.name.value or selection.name.value
+        local currentType = context.objects[#context.objects].fields[selection.name.value].kind
+        validateSelection(selectionKey, currentType)
+      elseif selection.kind == 'inlineFragment' then
+        validateSelectionSet(selection.selectionSet)
+      elseif selection.kind == 'fragmentSpread' then
+        validateSelectionSet(selection.selectionSet)
+      end
+    end
+  end
+
+  validateSelectionSet(node)
+end
+
+return rules

+ 95 - 65
validate.lua

@@ -1,99 +1,129 @@
-return function(schema, tree)
-
-  local context = {
-    operationNames = {},
-    hasAnonymousOperation = false,
-    typeStack = {}
-  }
+local rules = require 'rules'
 
-  local visitors = {
-    document = function(node)
+local visitors = {
+  document = {
+    children = function(node, context)
       return node.definitions
-    end,
-
-    operation = function(node)
-      local name = node.name and node.name.value
+    end
+  },
 
-      if name then
-        if context.operationNames[name] then
-          error('Multiple operations exist named "' .. name .. '"')
-        else
-          context.operationNames[name] = true
-        end
-      else
-        if context.hasAnonymousOperation or next(context.operationNames) then
-          error('Cannot have more than one operation when using anonymous operations')
-        end
+  operation = {
+    enter = function(node, context)
+      table.insert(context.objects, context.schema.query)
+    end,
 
-        context.hasAnonymousOperation = true
-      end
+    exit = function(node, context)
+      table.remove(context.objects)
+    end,
 
-      return {node.selectionSet}
+    children = function(node)
+      return { node.selectionSet }
     end,
 
-    selectionSet = function(node)
+    rules = {
+      rules.uniqueOperationNames,
+      rules.loneAnonymousOperation
+    }
+  },
+
+  selectionSet = {
+    children = function(node)
       return node.selections
     end,
 
-    field = function(node)
-      local currentType = context.typeStack[#context.typeStack].__type
-      if currentType == 'Scalar' and node.selectionSet then
-        error('Scalar values cannot have subselections')
-      end
+    rules = { rules.unambiguousSelections }
+  },
 
-      local isCompositeType = currentType == 'Object' or currentType == 'Interface' or currentType == 'Union'
-      if isCompositeType and not node.selectionSet then
-        error('Composite types must have subselections')
-      end
+  field = {
+    enter = function(node, context)
+      local parentField = context.objects[#context.objects].fields[node.name.value]
+
+      -- false is a special value indicating that the field was not present in the type definition.
+      context.currentField = parentField and parentField.kind or false
 
+      table.insert(context.objects, context.currentField)
+    end,
+
+    exit = function(node, context)
+      table.remove(context.objects)
+      context.currentField = nil
+    end,
+
+    children = function(node)
       if node.selectionSet then
         return {node.selectionSet}
       end
     end,
 
-    inlineFragment = function(node)
+    rules = {
+      rules.fieldsDefinedOnType,
+      rules.argumentsDefinedOnType,
+      rules.scalarFieldsAreLeaves,
+      rules.compositeFieldsAreNotLeaves
+    }
+  },
+
+  inlineFragment = {
+    enter = function(node, context)
+      local kind = false
+
+      if node.typeCondition then
+        kind = context.schema:getType(node.typeCondition.name.value) or false
+      end
+
+      table.insert(context.objects, kind)
+    end,
+
+    exit = function(node, context)
+      table.remove(context.objects)
+    end,
+
+    children = function(node, context)
       if node.selectionSet then
         return {node.selectionSet}
       end
-    end
+    end,
+
+    rules = { rules.inlineFragmentValidTypeCondition }
+  }
+}
+
+return function(schema, tree)
+  local context = {
+    operationNames = {},
+    hasAnonymousOperation = false,
+    objects = {},
+    schema = schema
   }
 
-  local root = schema.query
   local function visit(node)
-    if node.kind and visitors[node.kind] then
-      if node.kind == 'operation' then
-        table.insert(context.typeStack, schema.query)
-      elseif node.kind == 'field' then
-        local parent = context.typeStack[#context.typeStack]
-        if parent.fields[node.name.value] then
-          table.insert(context.typeStack, parent.fields[node.name.value].kind)
-        else
-          error('Field "' .. node.name.value .. '" is not defined on type "' .. parent.name .. '"')
-        end
-      elseif node.kind == 'inlineFragment' then
-        if node.typeCondition then
-          local kind = schema:getType(node.typeCondition.name.value)
+    local visitor = node.kind and visitors[node.kind]
 
-          if not kind then
-            error('Inline fragment type condition refers to non-existent type')
-          end
+    if not visitor then return end
 
-          if kind and kind.__type ~= 'Object' and kind.__type ~= 'Interface' and kind.__type ~= 'Union' then
-            error('Inline fragment type condition was not an Object, Interface, or Union')
-          end
+    if visitor.enter then
+      visitor.enter(node, context)
+    end
 
-          table.insert(context.typeStack, kind)
-        end
+    if visitor.rules then
+      for i = 1, #visitor.rules do
+        visitor.rules[i](node, context)
       end
+    end
 
-      local targets = visitors[node.kind](node)
-      if targets then
-        for _, target in ipairs(targets) do
-          visit(target)
+    if visitor.children then
+      local children = visitor.children(node)
+      if children then
+        for _, child in ipairs(children) do
+          visit(child)
         end
       end
     end
+
+    if visitor.exit then
+      visitor.exit(node, context)
+    end
   end
 
-  visit(tree)
+  return visit(tree)
 end