packages feed

nirum-0.5.0: src/Nirum/Targets/Python/Deserializers.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
module Nirum.Targets.Python.Deserializers
    ( compileDeserializer
    ) where

import Text.Blaze (Markup)
import Text.Heterocephalus (compileText)
import Text.InterpolatedString.Perl6 (qq)

import Nirum.Constructs.Identifier
import Nirum.Constructs.ModulePath
import Nirum.Constructs.TypeDeclaration
import Nirum.Constructs.TypeExpression
import Nirum.Package.Metadata
import Nirum.Targets.Python.CodeGen
import Nirum.Targets.Python.Validators
import Nirum.TypeInstance.BoundModule

-- | It takes these arguments:
--
--     1. a 'BoundModule Python' for the context,
--     2. a 'TypeExpression' which determines how it interprets an input,
--     3. a Python expression of an input,
--     4. a Python expression to store an output, and
--     5. a Python expression of a function, takes two arguments, to store a
--        possible error.
--
-- It returns a Python code that deserializes an input.
--
-- A Python expressions of the third argument should be a target (lvalue)
-- expression, and the last argument should be evaluated as a Python callable.
--
-- A generated Python can be more than an expression; it can
-- consists of multiple statements.  Note that its indentation level is zero.
--
-- For example, if we have a deserializer named @d@ the following code:
--
-- @
--     d "inputs['x']" "outputs['x']" "error"
-- @
--
-- could return a string containing a Python code like the following:
--
-- @
--     if isinstance(inputs['x'], str):
--         try:
--             outputs['x'] = int(inputs['x'])
--         except ValueError:
--             errors('', 'Expected a numeric string.')
--     else:
--         error('', 'Expected a string.')
-- @
compileDeserializer
    :: BoundModule Python -- | The compilation context.
    -> TypeExpression -- | Determines how it interprets an input.
    -> Code -- | A Python expression of an input.
    -> Code -- | A Python expression to store an output.
    -> Code -- | A Python expression of a function to store a possible error.
    -> CodeGen Markup -- | A Python code that deserializes an input.
compileDeserializer mod' typeExpr vInput vOutput vError = do
    let intermOutput = mangleVar vOutput "interm"
    let intermError = mangleVar vError "interm"
    deserializer <- compileDeserializer'
        mod' typeExpr vInput [qq|$intermOutput['rv']|] intermError
    Validator pred' valueValidators' <-
        compileValidator mod' typeExpr [qq|$intermOutput['rv']|]
    return [compileText|
#{intermOutput} = {}
def #{intermError}(err_field, err_msg):
    #{intermError}.errored = True
    #{vError}(err_field, err_msg)
#{deserializer}
if not getattr(#{intermError}, 'errored', False) and #{intermOutput}:
    if not (#{pred'}):
        (#{vError})(
            '',
            'A deserialized value is unexpected type: {0!r}.'.format(
                #{intermOutput}['rv']
            )
        )
%{ forall ValueValidator predCode errorMsg <- valueValidators' }
    elif not (#{predCode}):
        (#{vError})('', #{stringLiteral errorMsg})
%{ endforall }
    (#{vOutput}) = #{intermOutput}['rv']
    |]

compileDeserializer'
    :: BoundModule Python -- | The compilation context.
    -> TypeExpression -- | Determines how it interprets an input.
    -> Code -- | A Python expression of an input.
    -> Code -- | A Python expression to store an output.
    -> Code -- | A Python expression of a function to store a possible error.
    -> CodeGen Markup -- | A Python code that deserializes an input.

compileDeserializer' mod' (OptionModifier typeExpr) vInput vOutput vError = do
    deserializer <- compileDeserializer mod' typeExpr vInput vOutput vError
    return [compileText|
if (#{vInput}) is None:
    (#{vOutput}) = None
else:
#{indent "    " deserializer}
    |]

compileDeserializer' mod' (SetModifier typeExpr) vInput vOutput vError = do
    let elInput = mangleVar vInput "element"
        elIndex = mangleVar vInput "index"
        elOutput = mangleVar vOutput "element"
        elError = mangleVar vError "element"
        elErrorFlag = mangleVar vError "errored"
    builtins <- importBuiltins
    baseString <- baseStringClass
    mAbc <- collectionsAbc
    deserializer <- compileDeserializer mod' typeExpr elInput elOutput elError
    return [compileText|
if (#{builtins}.isinstance(#{vInput}, #{mAbc}.Sequence) and
    not #{builtins}.isinstance(#{vInput}, #{baseString})):
    (#{vOutput}) = #{builtins}.set()
    #{elErrorFlag} = [False]
    def #{elError}(err_field, err_msg):
        #{elErrorFlag}[0] = True
        #{vError}(
            '[{0}]{1}'.format(#{elIndex}, err_field), err_msg
        )
    for #{elIndex}, #{elInput} in #{builtins}.enumerate(#{vInput}):
        #{elErrorFlag}[0] = False
#{indent "        " deserializer}
        if not #{elErrorFlag}[0]:
            (#{vOutput}).add(#{elOutput})
    (#{vOutput}) = #{builtins}.frozenset(#{vOutput})
else:
    (#{vError})('', 'Expected an array.')
    |]

compileDeserializer' mod' (ListModifier typeExpr) vInput vOutput vError = do
    let elInput = mangleVar vInput "elem"
        elIndex = mangleVar vInput "idx"
        elOutput = mangleVar vOutput "elem"
        elError = mangleVar vError "elem"
        elErrorFlag = mangleVar vError "flag"
    builtins <- importBuiltins
    mAbc <- collectionsAbc
    baseString <- baseStringClass
    insertThirdPartyImportsA [("nirum.datastructures", [("list_type", "List")])]
    deserializer <- compileDeserializer mod' typeExpr elInput elOutput elError
    return [compileText|
if (#{builtins}.isinstance(#{vInput}, #{mAbc}.Sequence) and
    not #{builtins}.isinstance(#{vInput}, #{baseString})):
    (#{vOutput}) = []
    #{elErrorFlag} = [False]
    def #{elError}(err_field, err_msg):
        #{elErrorFlag}[0] = True
        #{vError}(
            '[{0}]{1}'.format(#{elIndex}, err_field), err_msg
        )
    for #{elIndex}, #{elInput} in #{builtins}.enumerate(#{vInput}):
        #{elErrorFlag}[0] = False
#{indent "        " deserializer}
        if not #{elErrorFlag}[0]:
            (#{vOutput}).append(#{elOutput})
    (#{vOutput}) = list_type(#{vOutput})
else:
    (#{vError})(('', 'Expected an array.'))
    |]

compileDeserializer' mod' (MapModifier keyTypeExpr valueTypeExpr)
                     vInput vOutput vError = do
    let pairInput = mangleVar vInput "pair"
        pairIndex = mangleVar vInput "index"
        pairErrored = mangleVar vInput "errored"
        keyInput = mangleVar vInput "key"
        keyOutput = mangleVar vOutput "key"
        keyError = mangleVar vError "key"
        valueInput = mangleVar vInput "value"
        valueOutput = mangleVar vOutput "value"
        valueError = mangleVar vError "value"
    mAbc <- collectionsAbc
    builtins <- importBuiltins
    baseString <- baseStringClass
    insertThirdPartyImportsA [("nirum.datastructures", [("map_type", "Map")])]
    keyDeserializer <- compileDeserializer
        mod' keyTypeExpr keyInput keyOutput keyError
    valueDeserializer <- compileDeserializer
        mod' valueTypeExpr valueInput valueOutput valueError
    return [compileText|
if (#{builtins}.isinstance(#{vInput}, #{mAbc}.Sequence) and
    not #{builtins}.isinstance(#{vInput}, #{baseString})):
    (#{vOutput}) = []
    (#{keyError}) = lambda err_field, err_msg: #{vError}(
        '[{0}].key{1}'.format(#{pairIndex}, err_field),
        err_msg
    )
    (#{valueError}) = lambda err_field, err_msg: #{vError}(
        '[{0}].value{1}'.format(#{pairIndex}, err_field),
        err_msg
    )
    for #{pairIndex}, #{pairInput} in #{builtins}.enumerate(#{vInput}):
        (#{pairErrored}) = False
        if #{builtins}.isinstance(#{pairInput}, #{mAbc}.Mapping):
            try:
                (#{keyInput}) = #{pairInput}['key']
            except #{builtins}.KeyError:
                (#{vError})(
                    '[{0}].key'.format(#{pairIndex}), 'Expected to exist.'
                )
                (#{pairErrored}) = True
            else:
#{indent "                " keyDeserializer}
            try:
                (#{valueInput}) = #{pairInput}['value']
            except #{builtins}.KeyError:
                (#{vError})(
                    '[{0}].value'.format(#{pairIndex}), 'Expected to exist.'
                )
                (#{pairErrored}) = True
            else:
#{indent "                " valueDeserializer}
            if not #{pairErrored}:
                (#{vOutput}).append((#{keyOutput}, #{valueOutput}))
        else:
            (#{vError})(
                '[{0}]'.format(#{pairIndex}),
                'Expected an object which has the fields "key" and "value".'
            )
    (#{vOutput}) = map_type(#{vOutput})
else:
    (#{vError})('', 'Expected an array of objects.')
    |]

compileDeserializer' mod' (TypeIdentifier typeId) vInput vOutput vError = do
    builtins <- importBuiltins
    case lookupType typeId mod' of
        Missing -> return [compileText|raise #{builtins}.RuntimeError(
            "This must never happen; it is likely to be a Nirum compiler's bug."
        )|]
        Local (Alias t) -> compileDeserializer mod' t vInput vOutput vError
        Imported modulePath' _ (Alias t) ->
            case resolveBoundModule modulePath' (boundPackage mod') of
                Nothing -> return [compileText|raise #{builtins}.RuntimeError(
                    "This must never happen; "
                    "it is likely to be a Nirum compiler's bug."
                )|]
                Just foundMod ->
                    compileDeserializer foundMod t vInput vOutput vError
        Local PrimitiveType { primitiveTypeIdentifier = p } ->
            compilePrimitiveTypeDeserializer p vInput vOutput vError
        Imported _ _ PrimitiveType { primitiveTypeIdentifier = p } ->
            compilePrimitiveTypeDeserializer p vInput vOutput vError
        Local EnumType {} -> deserializerCall Nothing typeId
        Imported m srcTypeId EnumType {} -> deserializerCall (Just m) srcTypeId
        Local RecordType {} -> deserializerCall Nothing typeId
        Imported m srcTypId RecordType {} -> deserializerCall (Just m) srcTypId
        Local UnboxedType {} -> deserializerCall Nothing typeId
        Imported m srcTypId UnboxedType {} -> deserializerCall (Just m) srcTypId
        Local UnionType {} -> deserializerCall Nothing typeId
        Imported m srcTypeId UnionType {} -> deserializerCall (Just m) srcTypeId
  where
    target' :: Python
    target' = packageTarget $ boundPackage mod'
    deserializerCall :: Maybe ModulePath -> Identifier -> CodeGen Markup
    deserializerCall m sourceTypeId = do
        case m of
            Just mp -> insertThirdPartyImportsA
                [ ( toImportPath target' mp
                  , [(toClassName typeId, toClassName sourceTypeId)]
                  )
                ]
            Nothing -> return ()
        return [compileText|
#{vOutput} = #{toClassName typeId}.__nirum_deserialize__(
    #{vInput},
    on_error=(#{vError})
)
        |]

compilePrimitiveTypeDeserializer :: PrimitiveTypeIdentifier
                                 -> Code
                                 -> Code
                                 -> Code
                                 -> CodeGen Markup

compilePrimitiveTypeDeserializer Bigint vInput vOutput vError = do
    builtins <- importBuiltins
    baseInteger <- baseIntegerClass
    baseString <- baseStringClass
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{baseString}):
    try:
        #{vOutput} = #{builtins}.int(#{vInput})
    except #{builtins}.ValueError:
        #{vError}(
            '',
            'Expected a string of decimal digits, '
            'but the string consists of other than decimal digits.'
        )
elif #{builtins}.isinstance(#{vInput}, #{baseInteger}):
    #{vOutput} = #{vInput}
else:
    #{vError}(
        '',
        'Expected a string of decimal digits, '
        'but the given value is not a string.'
    )
    |]

compilePrimitiveTypeDeserializer Decimal vInput vOutput vError = do
    builtins <- importBuiltins
    decimal <- importStandardLibrary "decimal"
    baseString <- baseStringClass
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{baseString}):
    try:
        #{vOutput} = #{decimal}.Decimal(#{vInput})
    except #{builtins}.ValueError:
        #{vError}(
            '', 'Expected a numeric string, but the string is not numeric.'
        )
else:
    #{vError}(
        '', 'Expected a numeric string, but the given value is not a string.'
    )
    |]

compilePrimitiveTypeDeserializer Int32 vInput vOutput vError = do
    baseInteger <- baseIntegerClass
    builtins <- importBuiltins
    baseString <- baseStringClass
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{baseInteger}):
    #{vOutput} = #{vInput}
elif #{builtins}.isinstance(#{vInput}, #{builtins}.float):
    #{vOutput} = #{builtins}.int(#{vInput})
    if #{vOutput} != (#{vInput}):
        #{vError}(
            '',
            'Expected an integral number, the given number is not integral.'
        )
elif #{builtins}.isinstance(#{vInput}, #{baseString}):
    try:
        #{vOutput} = #{builtins}.int(#{vInput})
    except #{builtins}.ValueError:
        #{vError}(
            '', 'Expected an integral number or a string of decimal digits.'
        )
else:
    #{vError}(
        '',
        'Expected an integral number, but the given value is not a number.'
    )
    |]

compilePrimitiveTypeDeserializer Int64 vInput vOutput vError =
    compilePrimitiveTypeDeserializer Int32 vInput vOutput vError

compilePrimitiveTypeDeserializer Float32 vInput vOutput vError = do
    builtins <- importBuiltins
    baseString <- baseStringClass
    baseInteger <- baseIntegerClass
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{builtins}.float):
    #{vOutput} = #{vInput}
elif (#{builtins}.isinstance(#{vInput}, #{baseString}) or
      #{builtins}.isinstance(#{vInput}, #{baseInteger})):
    try:
        #{vOutput} = #{builtins}.int(#{vInput})
    except #{builtins}.ValueError:
        #{vError}(
            '',
            'Expected a floating-point number or its string representation.'
        )
else:
    #{vError}(
        '',
        'Expected a floating-point number, '
        'but the given value is not a number.'
    )
    |]

compilePrimitiveTypeDeserializer Float64 vInput vOutput vError =
    compilePrimitiveTypeDeserializer Float32 vInput vOutput vError

compilePrimitiveTypeDeserializer Text vInput vOutput vError = do
    builtins <- importBuiltins
    pyVer <- getPythonVersion
    case pyVer of
        Python3 -> return [compileText|
if #{builtins}.isinstance(#{vInput}, #{builtins}.str):
    #{vOutput} = #{vInput}
else:
    #{vError}('', 'Expected a string.')
        |]
        Python2 -> return [compileText|
if #{builtins}.isinstance(#{vInput}, #{builtins}.unicode):
    #{vOutput} = #{vInput}
elif #{builtins}.isinstance(#{vInput}, str):
    try:
        #{vOutput} = (#{vInput}).decode('utf-8')
    except #{builtins}.UnicodeDecodeError:
        try:
            #{vOutput} = unicode(#{vInput})
        except #{builtins}.UnicodeDecodeError:
            #{vError}('', 'Expected a Unicode string.')
else:
    #{vError}('', 'Expected a string.')
        |]

compilePrimitiveTypeDeserializer Binary vInput vOutput vError = do
    builtins <- importBuiltins
    base64 <- importStandardLibrary "base64"
    baseString <- baseStringClass
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{baseString}):
    try:
        #{vOutput} = #{base64}.b64decode(#{vInput})
    except #{builtins}.ValueError:
        #{vError}(
            '',
            'Expected a base64-encoded string, '
            'but the string is not properly base64-encoded.'
        )
else:
    #{vError}(
        '',
        'Expected a base64-encoded string, but the given value is not a string'
    )
    |]

compilePrimitiveTypeDeserializer Date vInput vOutput vError = do
    builtins <- importBuiltins
    datetime <- importStandardLibrary "datetime"
    re <- importStandardLibrary "re"
    let vMatch = mangleVar vInput "match"
    baseString <- baseStringClass
    wrapScope vInput vOutput vError $ \ in' _ err -> return [compileText|
if not #{builtins}.isinstance(#{in'}, #{baseString}):
    #{err}(
        '',
        'Expected a string of RFC 3339 date, '
        'but the given value is not a string.'
    )
#{vMatch} = #{re}.match(r'^(\d{4})-(\d\d)-(\d\d)$', #{in'})
if not #{vMatch}:
    #{err}(
        '',
        'Expected a string of RFC 3339 date, '
        'but the string is not a valid RFC 3339 date format.'
    )
try:
    return #{datetime}.date(
        #{builtins}.int(#{vMatch}.group(1)),
        #{builtins}.int(#{vMatch}.group(2)),
        #{builtins}.int(#{vMatch}.group(3)),
    )
except #{builtins}.ValueError:
    #{err}(
        '',
        'Expected a string of RFC 3339 date, but the date is invalid.'
    )
    |]

compilePrimitiveTypeDeserializer Datetime vInput vOutput vError = do
    builtins <- importBuiltins
    datetime <- importStandardLibrary "datetime"
    pyVer <- getPythonVersion
    case pyVer of
        Python3 -> do
            re <- importStandardLibrary "re"
            let vMatch = mangleVar vInput "match"
                vTz = mangleVar vInput "tz"
            wrapScope vInput vOutput vError $ \ in' _ err ->
                return [compileText|
if not #{builtins}.isinstance(#{in'}, #{builtins}.str):
    #{err}(
        '',
        'Expected a string of RFC 3339 date & time, '
        'but the given value is not a string.'
    )
#{vMatch} = #{re}.match(
    r'^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)'
    r'(?:\.(\d*))?'
    r'(Z|([+-])(\d\d):?(\d\d))$',
    #{in'}
)
if not #{vMatch}:
    #{err}(
        '',
        'Expected a string of RFC 3339 date & time, '
        'but the string is not a valid RFC 3339 date & time format.'
    )
try:
    if #{vMatch}.group(8) == 'Z':
        #{vTz} = #{datetime}.timezone.utc
    else:
        #{vTz} = #{datetime}.timezone(
            #{datetime}.timedelta(
                hours=#{builtins}.int(#{vMatch}.group(10)),
                minutes=#{builtins}.int(#{vMatch}.group(11))
            )
        )
        if #{vMatch}.group(9) == '-':
            #{vTz} = -#{vTz}
    return #{datetime}.datetime(
        #{builtins}.int(#{vMatch}.group(1)),
        #{builtins}.int(#{vMatch}.group(2)),
        #{builtins}.int(#{vMatch}.group(3)),
        #{builtins}.int(#{vMatch}.group(4)),
        #{builtins}.int(#{vMatch}.group(5)),
        #{builtins}.int(#{vMatch}.group(6)),
        #{builtins}.int((#{vMatch}.group(7) or '0')[:6].zfill(6)),
        tzinfo=#{vTz}
    )
except #{builtins}.ValueError:
    #{err}(
        '',
        'Expected a string of RFC 3339 date & time, '
        'but the date & time is invalid.'
    )
                |]
        Python2 -> do
            addOptionalDependency (3, 0) "iso8601"
            insertThirdPartyImportsA
                [("iso8601", [("_parse_iso8601", "parse_date")])]
            insertThirdPartyImportsA
                [("iso8601.iso8601", [("_iso8601_parse_error", "ParseError")])]
            let vException = mangleVar vError "exc"
            wrapScope vInput vOutput vError $ \ in' _ err ->
                return [compileText|
if not #{builtins}.isinstance(#{in'}, #{builtins}.basestring):
    #{err}(
        '',
        'Expected a string of RFC 3339 date & time, '
        'but the given value is not a string.'
    )
try:
    return _parse_iso8601(#{in'})
except _iso8601_parse_error as #{vException}:
    if #{builtins}.str(#{vException}).startswith('Unable to parse'):
        #{err}(
            '',
            'Expected a string of RFC 3339 date & time, '
            'but the string is not a valid RFC 3339 date & time format.'
        )
    else:
        #{err}(
            '',
            'Expected a string of RFC 3339 date & time, '
            'but the date & time is invalid.'
        )
                |]

compilePrimitiveTypeDeserializer Bool vInput vOutput vError = do
    builtins <- importBuiltins
    return [compileText|
if #{builtins}.isinstance(#{vInput}, #{builtins}.bool):
    #{vOutput} = #{vInput}
else:
    #{vError}('', 'Expected a boolean value; true or false.')
    |]

compilePrimitiveTypeDeserializer Uuid vInput vOutput vError =
    wrapScope vInput vOutput vError $ \ in' _ err -> do
        builtins <- importBuiltins
        uuid <- importStandardLibrary "uuid"
        baseString <- baseStringClass
        return [compileText|
if not #{builtins}.isinstance(#{in'}, #{baseString}):
    #{err}(
        '', 'Expected a string of UUID, but the given value is not a string.'
    )
try:
    return #{uuid}.UUID(#{in'})
except #{builtins}.ValueError:
    #{err}(
        '',
        'Expected a string of UUID, '
        'but the string is not a valid hexadecimal UUID format.'
    )
        |]

compilePrimitiveTypeDeserializer Url vInput vOutput vError =
    compilePrimitiveTypeDeserializer Text vInput vOutput vError

-- | Wrap a Python deserializer code block into an isolated scope.
--
-- Without this function, a deserializer code cannot @return@ or @raise@,
-- because it is embeded in the middle of other Python code which shares
-- the flow.  It it controls its flow using @return@ or @raise@ the other
-- code that embeds it also can be terminated together.
--
-- That means a deserializer code should not be like:
--
-- @
--     if not condition_1:
--         #{vError}('', 'It should satisfy the condition_1.')
--     elif not condition_2:
--         #{vError}('', 'It should satisfy the condition_2.')
--     if mode:
--         try:
--             return result()
--         except ValueError:
--             #{vError}('', 'A value is invalid')
--     return result2()
-- @
--
-- but instead be:
--
-- @
--     if condition_1:
--         if not condition_2:
--             if mode:
--                 try:
--                     #{vOutput} = result()
--                 except ValueError:
--                     #{vError}('', 'A value is invalid')
--              else:
--                  #{vOutput} = result2()
--         else:
--             #{vError}('', 'It should satisfy the condition_2.')
--     else:
--         #{vError}('', 'It should satisfy the condition_1.')
-- @
--
-- which is too much indented and using assignig a result to the output
-- variable instead of just returning it.  It is fragile and bug-prone since
-- we can use @return@ or @raise@ by mistake.
--
-- The 'wrapScope' function offers an isolated flow, i.e., a function scope.
-- With this, returning a value is equivalent to assigning it to the output
-- variable, and raising an exception is calling an error function.
wrapScope :: Code -> Code -> Code
          -> (Code -> Code -> Code -> CodeGen Markup)
          -> CodeGen Markup
wrapScope vInput vOutput vError callback = do
    let f = mangleVar vInput "scoped_func"
    let exc = mangleVar vError "exc"
    let scopedInput = mangleVar vInput "scoped"
    let scopedOutput = mangleVar vOutput "scoped"
    let scopedError = mangleVar vError "scoped"
    builtins <- importBuiltins
    scopedCode <- callback scopedInput scopedOutput scopedError
    return [compileText|
def #{f}(#{scopedInput}):
    def #{scopedError}(err_field, err_msg):
        raise ValueError(err_field, err_msg)
    #{indent "    " scopedCode}
    return #{scopedOutput}
try:
    #{vOutput} = #{f}(#{vInput})
except #{builtins}.ValueError as #{exc}:
    if #{builtins}.len(#{exc}.args) == 2:
        #{vError}(*#{exc}.args)
    else:
        raise
|]