Skip to content

Commit 9574053

Browse files
authored
Merge pull request #626 from hackworthltd/brprice/openapi-test
test: ToJSON/ToSchema instances agree
2 parents 54c5cb7 + 8c6ce4f commit 9574053

File tree

9 files changed

+274
-55
lines changed

9 files changed

+274
-55
lines changed

flake.nix

+1
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@
533533
"primer/test/outputs"
534534
"primer-service/test/outputs"
535535
".buildkite/"
536+
"primer-service/primer-service.cabal"
536537
];
537538
};
538539

primer-service/primer-service.cabal

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cabal-version: 2.4
1+
cabal-version: 3.0
22
name: primer-service
33
version: 0.7.2.0
44
license: AGPL-3.0-or-later
@@ -13,17 +13,15 @@ category: Web
1313
library
1414
exposed-modules:
1515
Primer.Client
16+
Primer.OpenAPI
1617
Primer.Pagination
1718
Primer.Servant.API
1819
Primer.Servant.OpenAPI
1920
Primer.Servant.Types
2021
Primer.Server
2122

2223
hs-source-dirs: src
23-
other-modules:
24-
Primer.OpenAPI
25-
Servant.OpenApi.OperationId
26-
24+
other-modules: Servant.OpenApi.OperationId
2725
default-language: GHC2021
2826
default-extensions:
2927
NoImplicitPrelude
@@ -40,6 +38,7 @@ library
4038
build-depends:
4139
, aeson >=2.0 && <=2.1
4240
, base >=4.12 && <=4.17
41+
, deriving-aeson >=0.2 && <=0.3
4342
, exceptions >=0.10.4 && <=0.11
4443
, http-client ^>=0.7.13
4544
, http-media >=0.8 && <=0.9
@@ -175,15 +174,19 @@ test-suite service-test
175174
, base
176175
, bytestring
177176
, hasql
177+
, hedgehog ^>=1.1.1
178+
, openapi3 >=3.2 && <=3.3
178179
, postgres-options ^>=0.2
179180
, pretty-simple ^>=4.0.0
180181
, primer
182+
, primer:primer-hedgehog
181183
, primer-rel8
182184
, primer-service
183185
, rel8 ^>=1.3
184186
, tasty ^>=1.4.1
185187
, tasty-discover ^>=4.2.4
186188
, tasty-golden ^>=2.3.5
189+
, tasty-hedgehog ^>=1.2.0
187190
, tasty-hunit ^>=0.10.0
188191
, temporary ^>=1.3
189192
, text

primer-service/src/Primer/OpenAPI.hs

+32-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1+
{-# LANGUAGE UndecidableInstances #-}
12
{-# OPTIONS_GHC -fno-warn-orphans #-}
23

34
module Primer.OpenAPI (
45
-- * Orphan instances
56
-- $orphanInstances
67
) where
78

8-
import Data.OpenApi (ToSchema)
9-
import Data.Text (Text)
10-
import Data.Typeable (Typeable)
9+
import Data.OpenApi (ToSchema (declareNamedSchema), fromAesonOptions, genericDeclareNamedSchema)
10+
import Data.OpenApi.Internal.Schema (GToSchema, rename)
11+
import Deriving.Aeson (AesonOptions (aesonOptions))
1112
import Primer.API (Def, Module, NodeBody, NodeFlavor, Prog, Tree)
12-
import Primer.Core (GlobalName, ID (..), LVarName, ModuleName)
13+
import Primer.Core (
14+
GlobalName,
15+
GlobalNameKind (ADefName, ATyCon, AValCon),
16+
ID (..),
17+
LVarName,
18+
ModuleName,
19+
)
1320
import Primer.Database (Session, SessionName)
21+
import Primer.JSON (CustomJSON, PrimerJSON)
1422
import Primer.Name (Name)
1523

24+
import Foreword
25+
1626
-- $orphanInstances
1727
--
1828
-- We define some OpenApi orphan instances in primer-service, to avoid
1929
-- pulling in the openapi3 dependency into primer core. This is necessary to
2030
-- build primer with ghcjs, because openapi3 transitively depends on network,
2131
-- which ghcjs currently cannot build.
2232

33+
-- Suitable for deriving via, when the ToJSON instance is via PrimerJSON
34+
instance
35+
(Typeable a, Generic a, GToSchema (Rep a), Typeable os, Typeable ks, AesonOptions os) =>
36+
ToSchema (CustomJSON (os :: ks) a)
37+
where
38+
declareNamedSchema _ = genericDeclareNamedSchema (fromAesonOptions (aesonOptions @os)) (Proxy @a)
39+
2340
instance ToSchema SessionName
2441
instance ToSchema Session
2542

@@ -31,14 +48,21 @@ deriving newtype instance ToSchema ID
3148
-- This instance works because the parameter has a phantom role!
3249
deriving via Text instance (ToSchema Name)
3350

34-
-- For GlobalName and LVarName, we must derive ToSchema via Name,
35-
-- as that is how the To/FromJSON instances are derived
36-
deriving via Name instance Typeable k => ToSchema (GlobalName k)
51+
-- For GlobalNames, we know the tag is just phantom type information
52+
-- and they all serialise in the same way. We collapse the distinction
53+
-- at the openapi level, so api consumers do not have to deal with
54+
-- three identical types. Note that our openapi interface is a
55+
-- simplified view, so this collapse is in the correct spirit.
56+
instance ToSchema (GlobalName 'ADefName) where
57+
declareNamedSchema _ = rename (Just "GlobalName") <$> declareNamedSchema (Proxy @(PrimerJSON (GlobalName 'ADefName)))
58+
deriving via GlobalName 'ADefName instance ToSchema (GlobalName 'ATyCon)
59+
deriving via GlobalName 'ADefName instance ToSchema (GlobalName 'AValCon)
60+
3761
deriving via Name instance (ToSchema LVarName)
3862
instance ToSchema Tree
3963
instance ToSchema NodeBody
4064
instance ToSchema NodeFlavor
4165
instance ToSchema Def
42-
instance ToSchema ModuleName
66+
deriving via NonEmpty Name instance ToSchema ModuleName
4367
instance ToSchema Module
4468
instance ToSchema Prog

primer-service/src/Primer/Pagination.hs

+29-11
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
module Primer.Pagination (
77
PaginationParams,
88
Pagination (..),
9-
Paginated,
9+
-- Constructor and field accessors of Pagination exported for testing
10+
Paginated (..),
1011
-- 'Positive' is abstract. Do not export its constructor.
1112
Positive (getPositive),
1213
mkPositive,
1314
pagedDefaultClamp,
1415
-- the following are exposed for testing
15-
meta,
16+
PaginatedMeta (PM),
1617
totalItems,
1718
pageSize,
1819
firstPage,
@@ -21,7 +22,8 @@ module Primer.Pagination (
2122
nextPage,
2223
lastPage,
2324
getNonNeg,
24-
items,
25+
NonNeg,
26+
mkNonNeg,
2527
) where
2628

2729
import Foreword
@@ -33,7 +35,9 @@ import Optics ((?~))
3335
import Primer.Database (
3436
OffsetLimit (OL, limit, offset),
3537
Page (Page, pageContents, total),
38+
Session,
3639
)
40+
import Primer.OpenAPI ()
3741
import Servant (
3842
DefaultErrorFormatters,
3943
ErrorFormatters,
@@ -59,7 +63,7 @@ import Servant.OpenApi (HasOpenApi (toOpenApi))
5963
-- @getPositive x > 0@ is always true (because the only way to create one is
6064
-- via the 'mkPositive' smart constructor.
6165
newtype Positive = Pos {getPositive :: Int}
62-
deriving (Eq, Ord)
66+
deriving (Eq, Ord, Show)
6367
deriving newtype (FromJSON, ToJSON)
6468

6569
mkPositive :: Int -> Maybe Positive
@@ -129,15 +133,25 @@ data Paginated a = Paginated
129133
{ meta :: PaginatedMeta
130134
, items :: [a]
131135
}
132-
deriving (Generic)
133-
134-
instance ToJSON a => ToJSON (Paginated a)
135-
instance FromJSON a => FromJSON (Paginated a)
136-
instance ToSchema a => ToSchema (Paginated a)
136+
deriving (Generic, Show)
137+
138+
-- We may well need more instances than just Paginated Session in the future.
139+
-- However, giving polymorphic `instance To... (Paginated a)` can generate
140+
-- a schema inconsistent with the ToJSON for some 'a'.
141+
-- This happens because aeson and openapi3 differ in their special handling
142+
-- for lists (e.g. to serialise strings as strings rather than arrays of
143+
-- characters). In particular the instance for 'Paginated Char' is broken.
144+
-- See https://github.com/biocad/openapi3/issues/58
145+
-- We prefer to explicitly list the particular instances we need, rather
146+
-- than having a known broken polymorphic instance, even if we expect to
147+
-- never hit the broken case.
148+
instance ToJSON (Paginated Session)
149+
instance FromJSON (Paginated Session)
150+
instance ToSchema (Paginated Session)
137151

138152
-- Used solely for nice bounds in schema
139153
newtype NonNeg = NonNeg Int
140-
deriving newtype (FromJSON, ToJSON)
154+
deriving newtype (FromJSON, ToJSON, Show)
141155
instance ToParamSchema NonNeg where
142156
toParamSchema _ = toParamSchema (Proxy @Int) & #minimum ?~ 0
143157
instance ToSchema NonNeg where
@@ -146,6 +160,10 @@ instance ToSchema NonNeg where
146160
getNonNeg :: NonNeg -> Int
147161
getNonNeg (NonNeg i) = i
148162

163+
-- For testing purposes
164+
mkNonNeg :: Int -> Maybe NonNeg
165+
mkNonNeg a = if a >= 0 then Just (NonNeg a) else Nothing
166+
149167
data PaginatedMeta = PM
150168
{ totalItems :: NonNeg
151169
, pageSize :: Positive
@@ -155,7 +173,7 @@ data PaginatedMeta = PM
155173
, nextPage :: Maybe Positive
156174
, lastPage :: Positive
157175
}
158-
deriving (Generic)
176+
deriving (Generic, Show)
159177
instance FromJSON PaginatedMeta
160178
instance ToJSON PaginatedMeta
161179
instance ToSchema PaginatedMeta

0 commit comments

Comments
 (0)