module Extism.Manifest where
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BS (unpack)
import Extism.JSON
import Text.JSON
import Text.JSON.Generic
-- | Memory options
data Memory = Memory
{ memoryMaxPages :: Nullable Int,
memoryMaxHttpResponseBytes :: Nullable Int,
memoryMaxVarBytes :: Nullable Int
}
deriving (Eq, Show)
instance JSON Memory where
showJSON (Memory max maxHttp maxVar) =
object
[ "max_pages" .= max,
"max_http_response_bytes" .= maxHttp,
"max_var_bytes" .= maxVar
]
readJSON obj =
let max = obj .? "max_pages"
httpMax = obj .? "max_http_response_bytes"
maxVar = obj .? "max_var_bytes"
in Ok (Memory max httpMax maxVar)
-- | HTTP request
data HTTPRequest = HTTPRequest
{ url :: String,
headers :: Nullable [(String, String)],
method :: Nullable String
}
deriving (Eq, Show)
makeKV x =
object [(k, showJSON v) | (k, v) <- x]
requestObj (HTTPRequest url headers method) =
[ "url" .= url,
"headers" .= mapNullable makeKV headers,
"method" .= method
]
instance JSON HTTPRequest where
showJSON req = object $ requestObj req
readJSON x =
let url = x .? "url"
headers = x .? "headers"
method = x .? "method"
in case url of
Null -> Error "Missing 'url' field"
NotNull url -> Ok (HTTPRequest url headers method)
-- | WASM from file
data WasmFile = WasmFile
{ filePath :: String,
fileName :: Nullable String,
fileHash :: Nullable String
}
deriving (Eq, Show)
instance JSON WasmFile where
showJSON (WasmFile path name hash) =
object
[ "path" .= path,
"name" .= name,
"hash" .= hash
]
readJSON x =
let path = x .? "url"
name = x .? "name"
hash = x .? "hash"
in case path of
Null -> Error "Missing 'path' field"
NotNull path -> Ok (WasmFile path name hash)
-- | WASM from raw bytes
data WasmData = WasmData
{ dataBytes :: Base64,
dataName :: Nullable String,
dataHash :: Nullable String
}
deriving (Eq, Show)
instance JSON WasmData where
showJSON (WasmData bytes name hash) =
object
[ "data" .= bytes,
"name" .= name,
"hash" .= hash
]
readJSON x =
let d = x .? "data"
name = x .? "name"
hash = x .? "hash"
in case d of
Null -> Error "Missing 'path' field"
NotNull d ->
case readJSON d of
Error msg -> Error msg
Ok d' -> Ok (WasmData d' name hash)
-- | WASM from a URL
data WasmURL = WasmURL
{ req :: HTTPRequest,
urlName :: Nullable String,
urlHash :: Nullable String
}
deriving (Eq, Show)
instance JSON WasmURL where
showJSON (WasmURL req name hash) =
object
( "name" .= name
: "hash" .= hash
: requestObj req
)
readJSON x =
let req = x .? "req"
name = x .? "name"
hash = x .? "hash"
in case fromNullable req of
Nothing -> Error "Missing 'req' field"
Just req -> Ok (WasmURL req name hash)
-- | Specifies where to get WASM module data
data Wasm = File WasmFile | Data WasmData | URL WasmURL deriving (Eq, Show)
instance JSON Wasm where
showJSON x =
case x of
File f -> showJSON f
Data d -> showJSON d
URL u -> showJSON u
readJSON x =
case file of
Ok x -> Ok (File x)
Error _ ->
let data' = (readJSON x :: Result WasmData)
in case data' of
Ok x -> Ok (Data x)
Error _ ->
let url = (readJSON x :: Result WasmURL)
in case url of
Ok x -> Ok (URL x)
Error _ -> Error "JSON does not match any of the Wasm types"
where
file = (readJSON x :: Result WasmFile)
wasmFile :: String -> Wasm
wasmFile path =
File WasmFile {filePath = path, fileName = null', fileHash = null'}
wasmURL :: String -> String -> Wasm
wasmURL method url =
let r = HTTPRequest {url = url, headers = null', method = nonNull method}
in URL WasmURL {req = r, urlName = null', urlHash = null'}
wasmData :: B.ByteString -> Wasm
wasmData d =
Data WasmData {dataBytes = Base64 d, dataName = null', dataHash = null'}
withName :: Wasm -> String -> Wasm
withName (Data d) name = Data d {dataName = nonNull name}
withName (URL url) name = URL url {urlName = nonNull name}
withName (File f) name = File f {fileName = nonNull name}
withHash :: Wasm -> String -> Wasm
withHash (Data d) hash = Data d {dataHash = nonNull hash}
withHash (URL url) hash = URL url {urlHash = nonNull hash}
withHash (File f) hash = File f {fileHash = nonNull hash}
-- | The 'Manifest' type is used to provide WASM data and configuration to the
-- | Extism runtime
data Manifest = Manifest
{ wasm :: [Wasm],
memory :: Nullable Memory,
config :: Nullable [(String, String)],
allowedHosts :: Nullable [String],
allowedPaths :: Nullable [(String, String)],
timeout :: Nullable Int
}
deriving (Eq, Show)
instance JSON Manifest where
showJSON (Manifest wasm memory config hosts paths timeout) =
let w = makeArray wasm
in object
[ "wasm" .= w,
"memory" .= memory,
"config" .= mapNullable makeKV config,
"allowed_hosts" .= hosts,
"allowed_paths" .= mapNullable makeKV paths,
"timeout_ms" .= timeout
]
readJSON x =
let wasm = x .? "wasm"
memory = x .? "memory"
config = x .? "config"
hosts = x .? "allowed_hosts"
paths = x .? "allowed_paths"
timeout = x .? "timeout_ms"
in case fromNullable wasm of
Nothing -> Error "Missing 'wasm' field"
Just wasm -> Ok (Manifest wasm memory config hosts paths timeout)
-- | Create a new 'Manifest' from a list of 'Wasm'
manifest :: [Wasm] -> Manifest
manifest wasm =
Manifest
{ wasm = wasm,
memory = null',
config = null',
allowedHosts = null',
allowedPaths = null',
timeout = null'
}
-- | Update the config values
withConfig :: Manifest -> [(String, String)] -> Manifest
withConfig m config =
m {config = nonNull config}
-- | Update allowed hosts for `extism_http_request`
withHosts :: Manifest -> [String] -> Manifest
withHosts m hosts =
m {allowedHosts = nonNull hosts}
-- | Update allowed paths
withPaths :: Manifest -> [(String, String)] -> Manifest
withPaths m p =
m {allowedPaths = nonNull p}
-- | Update plugin timeout (in milliseconds)
withTimeout :: Manifest -> Int -> Manifest
withTimeout m t =
m {timeout = nonNull t}
-- | Set memory.max_pages
withMaxPages :: Manifest -> Int -> Manifest
withMaxPages m pages =
case memory m of
Null ->
m {memory = NotNull $ Memory (NotNull pages) Null Null}
NotNull (Memory _ x y) ->
m {memory = NotNull $ Memory (NotNull pages) x y}
-- | Set memory.max_http_response_bytes
withMaxHttpResponseBytes :: Manifest -> Int -> Manifest
withMaxHttpResponseBytes m max =
case memory m of
Null ->
m {memory = NotNull $ Memory Null (NotNull max) Null}
NotNull (Memory x _ y) ->
m {memory = NotNull $ Memory x (NotNull max) y}
-- | Set memory.max_var_bytes
withMaxVarBytes :: Manifest -> Int -> Manifest
withMaxVarBytes m max =
case memory m of
Null ->
m {memory = NotNull $ Memory Null Null (NotNull max)}
NotNull (Memory x y _) ->
m {memory = NotNull $ Memory x y (NotNull max)}
fromString :: String -> Either String Manifest
fromString s = do
let x = decode s
resultToEither x
fromFile :: FilePath -> IO (Either String Manifest)
fromFile path = do
s <- readFile path
return $ fromString s
toString :: Manifest -> String
toString = encode
toFile :: FilePath -> Manifest -> IO ()
toFile path m = do
writeFile path (toString m)