graphql-engine/server/src-test/Control/Monad/CircularSpec.hs
Antoine Leblanc 3a400fab3d Rewrite OpenAPI
### Description

This PR rewrites OpenAPI to be more idiomatic. Some noteworthy changes:
- we accumulate all required information during the Analyze phase, to avoid having to do a single lookup in the schema cache during the OpenAPI generation phase (we now only need the schema cache as input to run the analysis)
- we no longer build intermediary endpoint information and aggregate it, we directly build the the `PathItem` for each endpoint; additionally, that means we no longer have to assume that different methods have the same metadata
- we no longer have to first declare types, then craft references: we do everything in one step
- we now properly deal with nullability by treating "typeName" and "typeName!" as different
- we add a bunch of additional fields in the generated "schema", such as title
- we do now support enum values in both input and output positions
- checking whether the request body is required is now performed on the fly rather than by introspecting the generated schema
- the methods in the file are sorted by topic

### Controversial point

However, this PR creates some additional complexity, that we might not want to keep. The main complexity is _knot-tying_: to avoid lookups when generating the OpenAPI, it builds an actual graph of input types, which means that we need something similar to (but simpler than) `MonadSchema`, to avoid infinite recursions when analyzing the input types of a query. To do this, this PR introduces `CircularT`, a lesser `SchemaT` that aims at avoiding ever having to reinvent this particular wheel ever again.

### Remaining work

- [x] fix existing tests (they are all failing due to some of the schema changes)
- [ ] add tests to cover the new features:
  - [x] tests for `CircularT`
  - [ ] tests for enums in output schemas
- [x] extract / document `CircularT` if we wish to keep it
- [x] add more comments to `OpenAPI`
- [x] have a second look at `buildVariableSchema`
- [x] fix all missing diagnostics in `Analyze`
- [x] add a Changelog entry?

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4654
Co-authored-by: David Overton <7734777+dmoverton@users.noreply.github.com>
GitOrigin-RevId: f4a9191f22dfcc1dccefd6a52f5c586b6ad17172
2022-06-30 12:57:09 +00:00

147 lines
4.4 KiB
Haskell

module Control.Monad.CircularSpec (spec) where
import Control.Concurrent
import Control.Monad.Circular
import Data.HashMap.Strict qualified as Map
import Data.List (nub)
import Hasura.Prelude
import Test.Hspec
spec :: Spec
spec = do
describe "circular graphs" checkCircularGraphs
describe "infinite lists" checkInfiniteLists
describe "memoization" checkMemoization
describe "does not protect against bad code" checkFailure
--------------------------------------------------------------------------------
-- Utils
runWithTimeLimit :: MonadIO m => IO a -> m (Maybe a)
runWithTimeLimit action = liftIO do
var <- newEmptyMVar
threadId <- forkIO do
value <- action
putMVar var $! value
result <-
foldr1 continueOnFail $ replicate 10 do
threadDelay 10000 -- 10ms
tryTakeMVar var
killThread threadId
pure result
where
continueOnFail step nextStep =
step >>= \case
Nothing -> nextStep
Just res -> pure (Just res)
succeedsWithinTimeLimit :: (MonadIO m, MonadFail m) => IO a -> m a
succeedsWithinTimeLimit action =
runWithTimeLimit action
`onNothingM` fail "failed to compute in reasonable time"
--------------------------------------------------------------------------------
-- Graphs
data Node = Node String [Node]
nodeName :: Node -> String
nodeName (Node n _) = n
instance Show Node where
show (Node name succs) = "Node " ++ show name ++ " " ++ show (nodeName <$> succs)
instance Eq Node where
Node n1 s1 == Node n2 s2 = n1 == n2 && map nodeName s1 == map nodeName s2
checkCircularGraphs :: Spec
checkCircularGraphs = do
it "builds A -> B -> C -> A" do
(a, b, c) <- succeedsWithinTimeLimit $ runCircularT @String do
let buildA = withCircular "a" do
b <- buildB
pure $ Node "a" [b]
buildB = withCircular "b" do
c <- buildC
pure $ Node "b" [c]
buildC = withCircular "c" do
a <- buildA
pure $ Node "c" [a]
(,,)
<$> buildA
<*> buildB
<*> buildC
a `shouldBe` Node "a" [b]
b `shouldBe` Node "b" [c]
c `shouldBe` Node "c" [a]
it "builds A -> A" do
a <- succeedsWithinTimeLimit $ runCircularT @String do
let buildA = withCircular "a" do
a <- buildA
pure $ Node "a" [a]
buildA
a `shouldBe` Node "a" [a]
--------------------------------------------------------------------------------
-- Infinite lists
checkInfiniteLists :: Spec
checkInfiniteLists = do
it "builds `x = 1 : x`" do
l <- succeedsWithinTimeLimit $ runCircularT do
let x = withCircular () do
y <- x
pure $ (1 :: Int) : y
x
take 5 l `shouldBe` [1, 1, 1, 1, 1]
it "builds `[0,1,2..]`" do
l <- succeedsWithinTimeLimit $ runCircularT do
let x = withCircular () do
y <- x
pure $ (0 :: Int) : map succ y
x
take 5 l `shouldBe` [0, 1, 2, 3, 4]
--------------------------------------------------------------------------------
-- Memoization
checkMemoization :: Spec
checkMemoization = do
it "memoizes fibo" do
(fibos, count) <- succeedsWithinTimeLimit $
flip runStateT mempty $ runCircularT @Int do
let fibo n = withCircular n do
modify $ Map.insertWith (+) n 1
case n of
0 -> pure 0
1 -> pure 1
_ -> (+) <$> fibo (n - 2) <*> fibo (n - 1)
traverse fibo [0 .. 20]
fibos !! 20 `shouldBe` (6765 :: Int)
nub (Map.elems count) `shouldBe` [1 :: Int]
--------------------------------------------------------------------------------
-- Failure
checkFailure :: Spec
checkFailure = do
it "unsuccessfully attempts to memoize Maybe" do
result <- runWithTimeLimit $ runCircularT do
let buildA = withCircular "a" do
mb <- buildB
-- a can only exist if b exists
pure $ mb <&> \b -> Node "a" [b]
buildB = withCircular "b" do
ma <- buildA
-- b can only exist if a exists
pure $ ma <&> \a -> Node "b" [a]
buildA :: CircularT String (Maybe Node) IO (Maybe Node)
result `shouldBe` Nothing
it "unsuccessfully attempts to build a self-referential int" do
result <- runWithTimeLimit $ runCircularT do
let go = withCircular () do
x <- go
pure $ if odd x then 1 else 0 :: Int
go
result `shouldBe` Nothing