-- | Types that describe route info. We use it to derive OpenApi schema or clients. module Mig.Core.Types.Info ( RouteInfo (..), RouteInput (..), Describe (..), noDescription, getInputType, RouteOutput (..), IsRequired (..), OutputSchema, InputSchema, SchemaDefs (..), emptySchemaDefs, toSchemaDefs, addRouteInput, setOutputMedia, setMethod, emptyRouteInfo, describeInfoInputs, -- * api updates addBodyInfo, addHeaderInfo, addOptionalHeaderInfo, addQueryInfo, addQueryFlagInfo, addOptionalInfo, addCaptureInfo, -- * checks routeHasQuery, routeHasOptionalQuery, routeHasQueryFlag, routeHasCapture, ) where import Data.List.Extra (firstJust) import Data.Map.Strict qualified as Map import Data.OpenApi (Definitions, Referenced, Schema, ToParamSchema (..), ToSchema (..), declareSchemaRef) import Data.OpenApi.Declare (runDeclare) import Data.Proxy import Data.String import Data.Text (Text) import GHC.TypeLits import Mig.Core.Class.MediaType import Network.HTTP.Types.Method import Network.HTTP.Types.Status -- | Information on route data RouteInfo = RouteInfo { method :: Maybe Method -- ^ http method , inputs :: [Describe RouteInput] -- ^ route inputs , output :: RouteOutput -- ^ route outputs , tags :: [Text] -- ^ open-api tags , description :: Text -- ^ open-api description , summary :: Text -- ^ open-api summary } deriving (Show, Eq) newtype IsRequired = IsRequired Bool deriving newtype (Show, Eq) -- | Values which have human-readable description. data Describe a = Describe { description :: Maybe Text , content :: a } deriving (Show, Eq) -- | no description provided noDescription :: a -> Describe a noDescription = Describe Nothing {-| Appends descriptiton for the info special name request-body is dedicated to request body input nd raw-input is dedicated to raw input -} describeInfoInputs :: [(Text, Text)] -> RouteInfo -> RouteInfo describeInfoInputs descs routeInfo = routeInfo{inputs = fmap addDesc routeInfo.inputs} where addDesc inp = Describe (Map.lookup (getInputName inp) descMap) inp.content getInputName inp = case inp.content of ReqBodyInput _ _ -> "request-body" RawBodyInput -> "raw-input" CaptureInput captureName _ -> captureName QueryInput _ queryName _ -> queryName QueryFlagInput queryName -> queryName HeaderInput _ headerName _ -> headerName descMap = Map.fromList descs -- | Route inputs data RouteInput = ReqBodyInput MediaType SchemaDefs | RawBodyInput | CaptureInput Text Schema | QueryInput IsRequired Text Schema | QueryFlagInput Text | HeaderInput IsRequired Text Schema deriving (Show, Eq) -- | Get input media-type getInputType :: RouteInfo -> Maybe MediaType getInputType route = firstJust (fromInput . (.content)) route.inputs where fromInput = \case ReqBodyInput ty _ -> Just ty _ -> Nothing -- | Input schema type InputSchema = SchemaDefs -- | Route output data RouteOutput = RouteOutput { status :: Status -- ^ http status , media :: MediaType -- ^ media type , schema :: OutputSchema -- ^ open-api schema } deriving (Show, Eq) -- | Output schema type OutputSchema = SchemaDefs -- | Schem definition with references to the used sub-values data SchemaDefs = SchemaDefs { defs :: Definitions Schema , ref :: Maybe (Referenced Schema) } deriving (Show, Eq) -- | Create schema definition toSchemaDefs :: forall a. (ToSchema a) => SchemaDefs toSchemaDefs = SchemaDefs defs (Just ref) where (defs, ref) = runDeclare (declareSchemaRef (Proxy @a)) mempty -- | An empty schema definition emptySchemaDefs :: SchemaDefs emptySchemaDefs = SchemaDefs mempty Nothing -- | Add route input to route info list of inputs addRouteInput :: RouteInput -> RouteInfo -> RouteInfo addRouteInput inp = addRouteInputWithDescriptiton (noDescription inp) -- | Adds route input with description addRouteInputWithDescriptiton :: Describe RouteInput -> RouteInfo -> RouteInfo addRouteInputWithDescriptiton inp routeInfo = routeInfo{inputs = inp : routeInfo.inputs} {-| Default empty route info. We update it as we construct the route with type-safe DSL. Almost all values are derived from type signatures -} emptyRouteInfo :: RouteInfo emptyRouteInfo = RouteInfo Nothing [] (RouteOutput ok200 "*/*" emptySchemaDefs) [] "" "" -- | Set http-method of the route setMethod :: Method -> MediaType -> RouteInfo -> RouteInfo setMethod method mediaType routeInfo = routeInfo { method = Just method , output = RouteOutput routeInfo.output.status mediaType emptySchemaDefs } -- | Set output meida-type for the route setOutputMedia :: MediaType -> RouteInfo -> RouteInfo setOutputMedia mediaType routeInfo = routeInfo{output = setMedia routeInfo.output} where setMedia outp = outp{media = mediaType} -- | Add parameter to the inputs of the route addParamInfoBy :: forall sym a. (KnownSymbol sym, ToParamSchema a) => (Text -> Schema -> RouteInput) -> RouteInfo -> RouteInfo addParamInfoBy cons = addRouteInput (cons (getName @sym) (toParamSchema (Proxy @a))) -- | Adds required header info to API schema addHeaderInfo :: forall sym a. (KnownSymbol sym, ToParamSchema a) => RouteInfo -> RouteInfo addHeaderInfo = addParamInfoBy @sym @a (HeaderInput (IsRequired True)) -- | Adds optional header info to API schema addOptionalHeaderInfo :: forall sym a. (KnownSymbol sym, ToParamSchema a) => RouteInfo -> RouteInfo addOptionalHeaderInfo = addParamInfoBy @sym @a (HeaderInput (IsRequired False)) -- | Adds required query info to API schema addQueryInfo :: forall sym a. (KnownSymbol sym, ToParamSchema a) => RouteInfo -> RouteInfo addQueryInfo = addParamInfoBy @sym @a (QueryInput (IsRequired True)) -- | Adds optional query info to API schema addOptionalInfo :: forall sym a. (KnownSymbol sym, ToParamSchema a) => RouteInfo -> RouteInfo addOptionalInfo = addParamInfoBy @sym @a (QueryInput (IsRequired False)) -- | Adds capture info to API schema addCaptureInfo :: forall sym a. (KnownSymbol sym, ToParamSchema a) => RouteInfo -> RouteInfo addCaptureInfo = addParamInfoBy @sym @a CaptureInput -- | Adds query flag to API schema addQueryFlagInfo :: forall sym. (KnownSymbol sym) => RouteInfo -> RouteInfo addQueryFlagInfo = addRouteInput (QueryFlagInput (getName @sym)) -- | Adds request body to API schema addBodyInfo :: forall ty a. (ToMediaType ty, ToSchema a) => RouteInfo -> RouteInfo addBodyInfo = addRouteInput (ReqBodyInput (toMediaType @ty) (toSchemaDefs @a)) --------------------------------------------- -- checks -- | Check that route has query with given name routeHasQuery :: Text -> RouteInfo -> Bool routeHasQuery expectedName = routeHasInput isQuery where isQuery = \case QueryInput (IsRequired True) name _ -> expectedName == name _ -> False -- | Check that route has query with given name routeHasOptionalQuery :: Text -> RouteInfo -> Bool routeHasOptionalQuery expectedName = routeHasInput isOptionalQuery where isOptionalQuery = \case QueryInput (IsRequired False) name _ -> expectedName == name _ -> False -- | Check that route has query with given name routeHasQueryFlag :: Text -> RouteInfo -> Bool routeHasQueryFlag expectedName = routeHasInput isQueryFlag where isQueryFlag = \case QueryFlagInput name -> expectedName == name _ -> False -- | Check that route has query with given name routeHasCapture :: Text -> RouteInfo -> Bool routeHasCapture expectedName = routeHasInput isCapture where isCapture = \case CaptureInput name _ -> expectedName == name _ -> False -- | Check that route has certain input routeHasInput :: (RouteInput -> Bool) -> RouteInfo -> Bool routeHasInput check info = any (check . (.content)) info.inputs --------------------------------------------- -- utils getName :: forall sym a. (KnownSymbol sym, IsString a) => a getName = fromString (symbolVal (Proxy @sym))