git.iko.soy/deriving-openapi3
on master help sign in
9 files changed+241−86
since splitsnapshots
unifiedsplit
.github/workflows/build.yaml+63 .gitignore−1 Changelog.md+3 LICENSE+21 deriving-openapi3.cabal+55 package.yaml−29 runDoctests.sh+9 src/Deriving/OpenApi.hs+89−52 stack.yaml+1−4
.github/workflows/build.yamlnew file+63
@@ -0,0 +1,63 @@
1+name: Build
2+
3+on:
4+ push:
5+ branches: [master]
6+ pull_request:
7+ branches: [master]
8+ release:
9+ types: [prereleased]
10+
11+jobs:
12+ check_versions:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@v2
16+
17+ - name: Check library version
18+ if: github.event_name == 'release'
19+ run: |
20+ if [ "$(awk -F":" '{if ($1 == "version") {gsub(/ /, "", $2); print $2}}' *.cabal)" != "${{ github.event.release.tag_name }}" ]; then
21+ echo "::error file=action.yaml::Library version does not match release version."
22+ exit 1
23+ fi
24+ Linux:
25+ needs: check_versions
26+ runs-on: ubuntu-latest
27+ steps:
28+ - uses: actions/checkout@v2
29+
30+ - name: Cache
31+ uses: actions/cache@v1
32+ with:
33+ path: |
34+ ~/.stack
35+ .stack-work
36+ key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }}-${{ hashFiles('**.cabal') }}
37+ restore-keys: |
38+ ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }}
39+ ${{ runner.os }}-stack
40+
41+ - name: Build
42+ shell: bash
43+ run: |
44+ stack build
45+
46+ - name: doctests
47+ shell: bash
48+ run: |
49+ ./runDoctests.sh
50+
51+ - name: Check Formatting
52+ shell: bash
53+ run: |
54+ stack install ormolu
55+ ormolu -o -XTypeApplications -o -XCPP -m check $(find src -type f -name "*.hs")
56+
57+ - name: Publish candidate
58+ shell: bash
59+ if: github.event_name == 'release'
60+ env:
61+ HACKAGE_KEY: ${{ secrets.HACKAGE_TOKEN }}
62+ run: |
63+ stack upload --pvp-bounds both --test-tarball --candidate .
.gitignore−1
@@ -1 +0,0 @@
1−deriving-openapi3.cabal
Changelog.mdnew file+3
@@ -0,0 +1,3 @@
1+# 0.1.0.0
2+
3+Initial release
LICENSEnew file+21
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2022 Ilya Kostyuchenko
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
deriving-openapi3.cabalnew file+55
@@ -0,0 +1,55 @@
1+cabal-version: 1.12
2+
3+-- This file has been generated from package.yaml by hpack version 0.34.5.
4+--
5+-- see: https://github.com/sol/hpack
6+
7+name: deriving-openapi3
8+version: 0.1.0.0
9+synopsis: DerivingVia for OpenAPI 3
10+description: DerivingVia for OpenAPI 3
11+category: JSON, Generics, OpenAPI
12+homepage: https://github.com/ilyakooo0/deriving-openapi3#readme
13+bug-reports: https://github.com/ilyakooo0/deriving-openapi3/issues
14+maintainer: Ilya Kostyuchenko <ilyakooo0@gmail.com>
15+license: MIT
16+license-file: LICENSE
17+build-type: Simple
18+extra-source-files:
19+ Changelog.md
20+
21+source-repository head
22+ type: git
23+ location: https://github.com/ilyakooo0/deriving-openapi3
24+
25+library
26+ exposed-modules:
27+ Deriving.OpenApi
28+ other-modules:
29+ Paths_deriving_openapi3
30+ hs-source-dirs:
31+ src
32+ default-extensions:
33+ AllowAmbiguousTypes
34+ ScopedTypeVariables
35+ KindSignatures
36+ DataKinds
37+ TypeOperators
38+ FlexibleInstances
39+ TypeApplications
40+ UndecidableInstances
41+ TypeFamilies
42+ PolyKinds
43+ OverloadedStrings
44+ DerivingStrategies
45+ DerivingVia
46+ FlexibleContexts
47+ ghc-options: -Weverything -Wno-star-is-type -Wno-unsafe -Wno-prepositive-qualified-module -Wno-safe -Wno-missing-safe-haskell-mode -Wno-missing-import-lists -Wno-implicit-prelude
48+ build-depends:
49+ aeson
50+ , base >=4.12
51+ , deriving-aeson >=0.2.2
52+ , lens
53+ , openapi3
54+ , text
55+ default-language: Haskell2010
package.yamldeleted−29
@@ -1,29 +0,0 @@
1−name: deriving-openapi3
2−version: 0.1.0
3−synopsis: DerivingVia for OpenAPI 3
4−# description:
5−maintainer: Ilya Kostyuchenko <ilyakooo0@gmail.com>
6−github: ilyakooo0/deriving-openapi3
7−ghc-options: -Wall
8−dependencies:
9− - base
10− - openapi3
11− - deriving-aeson
12− - aeson
13− - servant
14− - lens
15− - text
16−library:
17− source-dirs: src
18−default-extensions:
19− - AllowAmbiguousTypes
20− - ScopedTypeVariables
21− - KindSignatures
22− - DataKinds
23− - TypeOperators
24− - FlexibleInstances
25− - TypeApplications
26− - UndecidableInstances
27− - TypeFamilies
28− - PolyKinds
29− - OverloadedStrings
runDoctests.shnew file+9
@@ -0,0 +1,9 @@
1+#!/bin/sh
2+
3+set -e
4+
5+EXTENSIONS="-XAllowAmbiguousTypes -XScopedTypeVariables -XKindSignatures -XDataKinds -XTypeOperators -XFlexibleInstances -XTypeApplications -XUndecidableInstances -XTypeFamilies -XPolyKinds"
6+
7+DEPENDENCIES="--package doctest --package yaml"
8+
9+stack exec $DEPENDENCIES -- find src -name '*.hs' -exec doctest $EXTENSIONS '{}' \;
src/Deriving/OpenApi.hs+89−52
@@ -1,18 +1,45 @@
1+{-# LANGUAGE CPP #-}
12{-# OPTIONS_GHC -Wno-orphans #-}
23
4+-- | __Example:__
5+--
6+-- >>> :set -XDerivingStrategies -XDerivingVia -XDeriveGeneric
7+--
8+-- >>> :{
9+-- data User = User
10+-- { userFirstName :: String,
11+-- userAge :: Maybe Integer
12+-- }
13+-- deriving stock (Generic)
14+-- deriving (FromJSON, ToJSON, ToSchema)
15+-- via CustomJSON
16+-- '[FieldLabelModifier '[StripPrefix "user", CamelToSnake], RejectUnknownFields]
17+-- User
18+-- showYaml = Data.ByteString.Char8.putStr . Data.Yaml.encode
19+-- :}
20+--
21+-- >>> showYaml $ toSchema (Proxy :: Proxy User)
22+-- type: object
23+-- properties:
24+-- first_name:
25+-- type: string
26+-- age:
27+-- type: integer
28+-- required:
29+-- - first_name
30+-- additionalProperties: false
331module Deriving.OpenApi
432 ( CustomOpenApi,
533 DatatypeNameModifier,
634 ToSchema,
735 module Deriving.Aeson,
8− Extending,
936 )
1037where
1138
1239import Control.Lens
1340import qualified Data.Aeson.Types as A
14−import Data.Kind
1541import Data.OpenApi
42+import Data.OpenApi.Internal.ParamSchema
1643import Data.OpenApi.Internal.Schema
1744import Data.Proxy
1845import Data.Text (Text)
@@ -21,82 +48,92 @@ import Data.Typeable
2148import Deriving.Aeson
2249import GHC.Generics
2350import GHC.TypeLits
24−import Servant.API
2551
26−type CustomOpenApi = CustomJSON
27−
28−instance (OpenApiOptions xs, GToSchema (Rep x), Generic x, Typeable x, Typeable xs) => ToSchema (CustomJSON xs x) where
29− declareNamedSchema Proxy =
30− openApiSchemaModifier @xs <$> genericDeclareNamedSchema (openApiOptions @xs) (Proxy @x)
31−
32−data DatatypeNameModifier t
33−
34−instance (StringModifier f, OpenApiOptions xs) => OpenApiOptions (DatatypeNameModifier f ': xs) where
35− openApiOptions = (openApiOptions @xs) {datatypeNameModifier = getStringModifier @f}
36−
37−instance (AesonOptions xs) => AesonOptions (DatatypeNameModifier f ': xs) where
38− aesonOptions = aesonOptions @xs
52+data Description (desc :: Symbol) = Description
3953
4054instance (AesonOptions xs) => AesonOptions (Description f ': xs) where
4155 aesonOptions = aesonOptions @xs
4256
43−instance (OpenApiOptions xs, KnownSymbol t) => OpenApiOptions (Description t ': xs) where
44− openApiOptions = openApiOptions @xs
57+instance KnownSymbol t => OpenApiOptionModifier (Description t) where
4558 openApiSchemaModifier = schema . description <>~ Just (toTextLine @t)
4659
4760toTextLine :: forall s. KnownSymbol s => Text
4861toTextLine = "\n\n" <> T.pack (symbolVal (Proxy @s))
4962
50−type family (++) (x :: [k]) (y :: [k]) :: [k] where
51− (++) '[] ys = ys
52− (++) (x ': xs) ys = xs ++ (x ': ys)
63+-- $setup
64+-- >>> import qualified Data.Yaml
65+-- >>> import qualified Data.ByteString.Char8
5366
54−type family Extending (c :: * -> *) (ee :: [*]) :: * -> * where
55− Extending (CustomJSON a) ee = CustomJSON (a ++ ee)
67+type CustomOpenApi = CustomJSON
5668
57−-- deriving-aeson-based instances
69+instance
70+ (OpenApiOptionModifier xs, GToSchema (Rep x), Generic x, Typeable x, Typeable xs, Typeable k) =>
71+ ToSchema (CustomJSON (xs :: k) x)
72+ where
73+ declareNamedSchema Proxy =
74+ openApiSchemaModifier @xs
75+ <$> genericDeclareNamedSchema (openApiOptionsModifier @xs defaultSchemaOptions) (Proxy @x)
5876
59−class OpenApiOptions (xs :: [Type]) where
60− openApiOptions :: SchemaOptions
77+instance
78+ (OpenApiOptionModifier xs, GToParamSchema (Rep x), Generic x) =>
79+ ToParamSchema (CustomJSON (xs :: k) x)
80+ where
81+ toParamSchema Proxy =
82+ genericToParamSchema (openApiOptionsModifier @xs defaultSchemaOptions) (Proxy @x)
83+
84+class OpenApiOptionModifier x where
85+ openApiOptionsModifier :: SchemaOptions -> SchemaOptions
86+ openApiOptionsModifier = id
6187 openApiSchemaModifier :: NamedSchema -> NamedSchema
6288 openApiSchemaModifier = id
6389
64−instance OpenApiOptions '[] where
65− openApiOptions = defaultSchemaOptions
90+data DatatypeNameModifier t
6691
67−instance OpenApiOptions xs => OpenApiOptions (UnwrapUnaryRecords ': xs) where
68− openApiOptions = (openApiOptions @xs) {unwrapUnaryRecords = True}
92+instance (StringModifier f) => OpenApiOptionModifier (DatatypeNameModifier f) where
93+ openApiOptionsModifier o = o {datatypeNameModifier = getStringModifier @f}
6994
70−instance OpenApiOptions xs => OpenApiOptions (OmitNothingFields ': xs) where
71− openApiOptions = openApiOptions @xs
95+instance (AesonOptions xs) => AesonOptions (DatatypeNameModifier f ': xs) where
96+ aesonOptions = aesonOptions @xs
7297
73−instance OpenApiOptions xs => OpenApiOptions (RejectUnknownFields ': xs) where
74− openApiOptions = openApiOptions @xs
98+-- deriving-aeson-based instances
99+
100+instance OpenApiOptionModifier UnwrapUnaryRecords where
101+ openApiOptionsModifier o = o {unwrapUnaryRecords = True}
102+
103+instance OpenApiOptionModifier OmitNothingFields
104+
105+#if MIN_VERSION_deriving_aeson(0,2,4)
106+instance OpenApiOptionModifier RejectUnknownFields where
75107 openApiSchemaModifier = schema . additionalProperties .~ Just (AdditionalPropertiesAllowed False)
108+#endif
76109
77−instance (StringModifier f, OpenApiOptions xs) => OpenApiOptions (FieldLabelModifier f ': xs) where
78− openApiOptions = (openApiOptions @xs) {fieldLabelModifier = getStringModifier @f}
110+instance StringModifier f => OpenApiOptionModifier (FieldLabelModifier f) where
111+ openApiOptionsModifier o = o {fieldLabelModifier = getStringModifier @f}
79112
80−instance (StringModifier f, OpenApiOptions xs) => OpenApiOptions (ConstructorTagModifier f ': xs) where
81− openApiOptions = (openApiOptions @xs) {constructorTagModifier = getStringModifier @f}
113+instance StringModifier f => OpenApiOptionModifier (ConstructorTagModifier f) where
114+ openApiOptionsModifier o = o {constructorTagModifier = getStringModifier @f}
82115
83116instance
84− (OpenApiOptions xs, TypeError ('Text "openapi3-deriving does not currently the `TagSingleConstructors` modifier.")) =>
85− OpenApiOptions (TagSingleConstructors ': xs)
86− where
87− openApiOptions = undefined
117+ TypeError ('Text "deriving-openapi3 does not currently the `TagSingleConstructors` modifier.") =>
118+ OpenApiOptionModifier TagSingleConstructors
119+
120+instance OpenApiOptionModifier NoAllNullaryToStringTag where
121+ openApiOptionsModifier o = o {allNullaryToStringTag = False}
122+
123+instance (KnownSymbol t, KnownSymbol c) => OpenApiOptionModifier (SumTaggedObject t c) where
124+ openApiOptionsModifier o = o {sumEncoding = A.TaggedObject (symbolVal (Proxy @t)) (symbolVal (Proxy @c))}
88125
89−instance OpenApiOptions xs => OpenApiOptions (NoAllNullaryToStringTag ': xs) where
90− openApiOptions = (openApiOptions @xs) {allNullaryToStringTag = False}
126+instance OpenApiOptionModifier SumUntaggedValue where
127+ openApiOptionsModifier o = o {sumEncoding = A.UntaggedValue}
91128
92−instance (KnownSymbol t, KnownSymbol c, OpenApiOptions xs) => OpenApiOptions (SumTaggedObject t c ': xs) where
93− openApiOptions = (openApiOptions @xs) {sumEncoding = A.TaggedObject (symbolVal (Proxy @t)) (symbolVal (Proxy @c))}
129+instance OpenApiOptionModifier SumObjectWithSingleField where
130+ openApiOptionsModifier o = o {sumEncoding = A.ObjectWithSingleField}
94131
95−instance (OpenApiOptions xs) => OpenApiOptions (SumUntaggedValue ': xs) where
96− openApiOptions = (openApiOptions @xs) {sumEncoding = A.UntaggedValue}
132+instance OpenApiOptionModifier SumTwoElemArray where
133+ openApiOptionsModifier o = o {sumEncoding = A.TwoElemArray}
97134
98−instance (OpenApiOptions xs) => OpenApiOptions (SumObjectWithSingleField ': xs) where
99− openApiOptions = (openApiOptions @xs) {sumEncoding = A.ObjectWithSingleField}
135+instance OpenApiOptionModifier '[]
100136
101−instance (OpenApiOptions xs) => OpenApiOptions (SumTwoElemArray ': xs) where
102− openApiOptions = (openApiOptions @xs) {sumEncoding = A.TwoElemArray}
137+instance (OpenApiOptionModifier x, OpenApiOptionModifier xs) => OpenApiOptionModifier (x ': xs) where
138+ openApiOptionsModifier = openApiOptionsModifier @xs . openApiOptionsModifier @x
139+ openApiSchemaModifier = openApiSchemaModifier @xs . openApiSchemaModifier @x
stack.yaml+1−4
@@ -1,7 +1,4 @@
1−resolver: lts-18.21
1+resolver: lts-18.23
22
33packages:
44 - .
5−
6−extra-deps:
7− - openapi3-3.1.0