hjsonschema-1.0.0.0: src/Data/JsonSchema/Fetch.hs
module Data.JsonSchema.Fetch where
import Import
-- Hiding is for GHCs before 7.10:
import Prelude hiding (concat, sequence)
import Control.Arrow (left)
import Control.Exception (catch)
import Control.Monad (foldM)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString as BS
import qualified Data.HashMap.Strict as H
import qualified Data.Text as T
import qualified Network.HTTP.Client as NC
import Data.Validator.Reference (resolveReference,
updateResolutionScope)
--------------------------------------------------
-- * Types
--------------------------------------------------
-- | This is all the fetching functions need to know about a particular
-- JSON Schema draft, e.g. JSON Schema Draft 4.
data FetchInfo schema = FetchInfo
{ _fiEmbedded :: schema -> ([schema], [schema])
, _fiId :: schema -> Maybe Text
, _fiRef :: schema -> Maybe Text
}
data ReferencedSchemas schema = ReferencedSchemas
{ _rsStarting :: !schema
-- ^ Used to resolve relative references when we don't know what the scope
-- of the current schema is. This only happens with starting schemas
-- because if we're using a remote schema we had to know its URI in order
-- to fetch it.
--
-- Tracking the starting schema (instead of just resolving the reference to
-- the current schema being used for validation) is necessary for cases
-- where schemas are embedded inside one another. For instance in this
-- case not distinguishing the starting and "foo" schemas sends the code
-- into an infinite loop:
--
-- {
-- "additionalProperties": false,
-- "properties": {
-- "foo": {
-- "$ref": "#"
-- }
-- }
-- }
, _rsSchemaMap :: !(URISchemaMap schema)
} deriving (Eq, Show)
-- | Keys are URIs (without URI fragments).
type URISchemaMap schema = HashMap Text schema
data SchemaWithURI schema = SchemaWithURI
{ _swSchema :: !schema
, _swURI :: !(Maybe Text)
-- ^ This is the URI identifying the document containing the schema.
-- It's different than the schema's "id" field, which controls scope
-- when resolving references contained in the schema.
-- TODO: Make the no URI fragment requirement unnecessary.
} deriving (Eq, Show)
getReference :: ReferencedSchemas schema -> Maybe Text -> Maybe schema
getReference referenced Nothing = Just (_rsStarting referenced)
getReference referenced (Just t) = H.lookup t (_rsSchemaMap referenced)
--------------------------------------------------
-- * Fetch via HTTP
--------------------------------------------------
data HTTPFailure
= HTTPParseFailure Text
| HTTPRequestFailure NC.HttpException
deriving Show
-- | Take a schema. Retrieve every document either it or its subschemas
-- include via the "$ref" keyword.
referencesViaHTTP'
:: forall schema. FromJSON schema
=> FetchInfo schema
-> SchemaWithURI schema
-> IO (Either HTTPFailure (URISchemaMap schema))
referencesViaHTTP' info sw = do
manager <- NC.newManager NC.defaultManagerSettings
let f = referencesMethodAgnostic (get manager) info sw
catch (left HTTPParseFailure <$> f) handler
where
get :: NC.Manager -> Text -> IO LBS.ByteString
get man url = do
request <- NC.parseUrlThrow (T.unpack url)
NC.responseBody <$> NC.httpLbs request man
handler
:: NC.HttpException
-> IO (Either HTTPFailure (URISchemaMap schema))
handler = pure . Left . HTTPRequestFailure
--------------------------------------------------
-- * Fetch via Filesystem
--------------------------------------------------
data FilesystemFailure
= FSParseFailure Text
| FSReadFailure IOError
deriving (Show, Eq)
referencesViaFilesystem'
:: forall schema. FromJSON schema
=> FetchInfo schema
-> SchemaWithURI schema
-> IO (Either FilesystemFailure (URISchemaMap schema))
referencesViaFilesystem' info sw = catch (left FSParseFailure <$> f) handler
where
f :: IO (Either Text (URISchemaMap schema))
f = referencesMethodAgnostic readFile' info sw
readFile' :: Text -> IO LBS.ByteString
readFile' = fmap LBS.fromStrict . BS.readFile . T.unpack
handler
:: IOError
-> IO (Either FilesystemFailure (URISchemaMap schema))
handler = pure . Left . FSReadFailure
--------------------------------------------------
-- * Method Agnostic Fetching Tools
--------------------------------------------------
-- | A version of 'fetchReferencedSchema's where the function to fetch
-- schemas is provided by the user. This allows restrictions to be added,
-- e.g. rejecting non-local URIs.
referencesMethodAgnostic
:: forall schema. FromJSON schema
=> (Text -> IO LBS.ByteString)
-> FetchInfo schema
-> SchemaWithURI schema
-> IO (Either Text (URISchemaMap schema))
referencesMethodAgnostic fetchRef info =
getRecursiveReferences fetchRef info mempty
getRecursiveReferences
:: forall schema. FromJSON schema
=> (Text -> IO LBS.ByteString)
-> FetchInfo schema
-> URISchemaMap schema
-> SchemaWithURI schema
-> IO (Either Text (URISchemaMap schema))
getRecursiveReferences fetchRef info referenced sw =
foldM f (Right referenced) (includeSubschemas info sw)
where
f :: Either Text (URISchemaMap schema)
-> SchemaWithURI schema
-> IO (Either Text (URISchemaMap schema))
f (Left e) _ = pure (Left e)
f (Right g) (SchemaWithURI schema mUri) =
case newRef of
Nothing -> pure (Right g)
Just uri -> do
bts <- fetchRef uri
case eitherDecode bts of
Left e -> pure . Left . T.pack $ e
Right schm -> getRecursiveReferences
fetchRef info (H.insert uri schm g)
(SchemaWithURI schm (Just uri))
where
newRef :: Maybe Text
newRef
| Just (Just uri,_) <- resolveReference mUri <$> _fiRef info schema
= case H.lookup uri g of
Nothing -> Just uri
Just _ -> Nothing
| otherwise = Nothing
-- | Return the schema passed in as an argument, as well as every
-- subschema contained within it.
includeSubschemas
:: forall schema.
FetchInfo schema
-> SchemaWithURI schema
-> [SchemaWithURI schema]
includeSubschemas info (SchemaWithURI schema mUri) =
SchemaWithURI schema mUri
: (includeSubschemas info =<< subSchemas)
where
subSchemas :: [SchemaWithURI schema]
subSchemas =
(\a -> SchemaWithURI a (updateResolutionScope mUri (_fiId info schema)))
<$> uncurry (<>) (_fiEmbedded info schema)