packages feed

acolyte-codegen-0.1.0.0: src/Acolyte/Codegen.hs

-- | @acolyte-codegen@ — generate API types from OpenAPI specs.
--
-- Reads Swagger 2.0 or OpenAPI 3.x JSON and produces Haskell modules
-- with type-level API definitions.
--
-- @
-- import Acolyte.Codegen
--
-- main = do
--   spec <- readSpecFile "openapi.json"
--   case spec of
--     Left err -> putStrLn (show err)
--     Right api -> putStrLn (T.unpack (emitModule defaultEmitConfig api))
-- @
module Acolyte.Codegen
  ( -- * Parsing
    parseSpec
  , ParseError (..)
  , readSpecFile
    -- * Proto parsing
  , parseProtoFile
  , parseProtoText
  , protoToIR
  , generateFromProto
  , ProtoFile (..)
  , ProtoService (..)
  , ProtoRpc (..)
  , ProtoMessage (..)
  , ProtoField (..)
  , ProtoEnum (..)
  , ProtoParseError (..)
    -- * Proto diffing
  , diffProtos
  , ProtoDiff (..)
  , DiffSeverity (..)
    -- * IR
  , ApiIR (..)
  , EndpointIR (..)
  , PathSegmentIR (..)
  , SchemaIR (..)
  , FieldIR (..)
  , HttpMethod (..)
    -- * Code generation
  , emitModule
  , EmitConfig (..)
  , defaultEmitConfig
  ) where

import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Yaml as Yaml
import Data.Text (Text)
import System.FilePath (takeExtension)

import Acolyte.Codegen.IR
import Acolyte.Codegen.Parse
import Acolyte.Codegen.Emit
import Acolyte.Codegen.Proto
import Acolyte.Codegen.ProtoDiff


-- | Read and parse a spec file. Supports both JSON and YAML.
--
-- File format is detected by extension:
-- * @.json@ → JSON parser
-- * @.yaml@, @.yml@ → YAML parser
-- * anything else → try YAML first (more permissive), fall back to JSON
readSpecFile :: FilePath -> IO (Either ParseError ApiIR)
readSpecFile path = do
  bytes <- BS.readFile path
  let ext = takeExtension path
      mVal = case ext of
        ".json" -> Aeson.decodeStrict' bytes
        ".yaml" -> either (const Nothing) Just (Yaml.decodeEither' bytes)
        ".yml"  -> either (const Nothing) Just (Yaml.decodeEither' bytes)
        _       -> case Yaml.decodeEither' bytes of
                     Right v  -> Just v
                     Left _   -> Aeson.decodeStrict' bytes
  case mVal of
    Nothing  -> pure (Left (InvalidJSON "Failed to decode file (tried JSON and YAML)"))
    Just val -> pure (parseSpec val)


-- | Generate Haskell code from a .proto file.
generateFromProto :: FilePath -> IO Text
generateFromProto path = do
  result <- parseProtoFile path
  case result of
    Left err -> error (show err)
    Right proto -> do
      let ir = protoToIR proto
      pure (emitModule defaultEmitConfig ir)