diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bf750e9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +max_line_length = 80 + +[{*.yml, *.yaml}] +indent_style = space +indent_size = 2 diff --git a/CHANGES.md b/CHANGES.md index f31b81a..f1cecf9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,9 @@ To be released. - The `uri` type has completly gone; use `url` instead. [[#126], [#281] by Jonghun Park] + - Added [`@numeric-constraints`](docs/annotation.md#numeric-constraints) + annotation to constraint the range of unboxed types' values. + [[#206], [#271] by Seunghun Lee] ### Docs target @@ -57,7 +60,9 @@ To be released. (from [spoqa/nirum](https://hub.docker.com/r/spoqa/nirum/)). [#126]: https://github.com/nirum-lang/nirum/issues/126 +[#206]: https://github.com/nirum-lang/nirum/issues/206 [#225]: https://github.com/nirum-lang/nirum/issues/225 +[#271]: https://github.com/nirum-lang/nirum/pull/271 [#281]: https://github.com/nirum-lang/nirum/pull/281 [#283]: https://github.com/spoqa/nirum/pull/283 [#297]: https://github.com/nirum-lang/nirum/issues/297 diff --git a/docs/annotation.md b/docs/annotation.md index a8a4ace..ed0b1a3 100644 --- a/docs/annotation.md +++ b/docs/annotation.md @@ -129,3 +129,32 @@ class FileNotFound(FileError): class FileNotReadable(FileError): ... ~~~~~~~~ + +### `@numeric-constraints` {#numeric-constraints} + +`@numeric-constraints` annotation constrains the range of unboxed types' values. +Currently, available annotation arguments are below: + +`min` +: Minimum input value; inclusive. + +`max` +: Maximum input value; inclusive. + +For example, the following first Nirum code is compiled to the second Python +code: + +~~~~~~~~ nirum +@numeric-constraints(min=1, max=12) +unboxed month (int32); +~~~~~~~~ + +~~~~~~~~ python +class Month: + def __init__(self, value: int) -> None: + if not value <= 12: + raise ValueError("value is greater than 12") + if not value >= 1: + raise ValueError("value is less than 1") + ... + ~~~~~~~~ diff --git a/src/Nirum/Targets/Python.hs b/src/Nirum/Targets/Python.hs index 6d1e5c4..4ccecf5 100644 --- a/src/Nirum/Targets/Python.hs +++ b/src/Nirum/Targets/Python.hs @@ -18,7 +18,7 @@ module Nirum.Targets.Python import Control.Monad (forM) import Control.Monad.State (modify) import qualified Data.List as L -import Data.Maybe (catMaybes, fromMaybe) +import Data.Maybe (catMaybes, fromMaybe, isJust) import GHC.Exts (IsList (toList)) import qualified Data.ByteString.Lazy @@ -685,12 +685,33 @@ compileTypeDeclaration src d@TypeDeclaration { typename = typename' |] compileTypeDeclaration src d@TypeDeclaration { typename = typename' , type' = UnboxedType itype + , typeAnnotations = annots } = do let className = toClassName' typename' itypeExpr <- compileTypeExpression' src (Just itype) insertStandardImport "typing" pyVer <- getPythonVersion - Validator typePred valueValidators' <- compileValidator' src itype "value" + Validator typePred valueValidatorsProto <- + compileValidator' src itype "value" + valueValidators' <- case A.lookup "numeric-constraints" annots of + Just A.Annotation { A.arguments = args } -> do + let constraintValidators = + [ case (name', value) of + ("min", Integer v) -> + Just $ ValueValidator + [qq|value >= ($v)|] + [qq|value is less than $v|] + ("max", Integer v) -> + Just $ ValueValidator + [qq|value <= ($v)|] + [qq|value is greater than $v|] + _ -> Nothing + | (name', value) <- toList args + ] + if all isJust constraintValidators + then return $ catMaybes constraintValidators + else fail "Unsupported arguments on @numeric-constraints" + Nothing -> return valueValidatorsProto deserializer <- compileDeserializer' src itype "value" "rv" "on_error" defaultErrorHandler <- defaultDeserializerErrorHandler return [compileText| diff --git a/test/Nirum/Targets/PythonSpec.hs b/test/Nirum/Targets/PythonSpec.hs index 58f968a..0189c4e 100644 --- a/test/Nirum/Targets/PythonSpec.hs +++ b/test/Nirum/Targets/PythonSpec.hs @@ -3,11 +3,15 @@ module Nirum.Targets.PythonSpec where import qualified Data.Map.Strict as M +import Data.Either import System.FilePath (()) import Test.Hspec.Meta +import Nirum.Constructs.Annotation hiding (null) +import Nirum.Constructs.Annotation.Internal import Nirum.Constructs.Module (Module (Module)) -import Nirum.Package.Metadata (Target (compilePackage)) +import Nirum.Constructs.TypeDeclaration hiding (Text) +import qualified Nirum.Package.Metadata as M import Nirum.Targets.Python ( Source (Source) , parseModulePath @@ -19,7 +23,7 @@ spec = do describe "compilePackage" $ do it "returns a Map of file paths and their contents to generate" $ do let (Source pkg _) = makeDummySource $ Module [] Nothing - files = compilePackage pkg + files = M.compilePackage pkg directoryStructure = [ "src-py2" "foo" "__init__.py" , "src-py2" "foo" "bar" "__init__.py" @@ -34,7 +38,7 @@ spec = do it "creates an emtpy Python package directory if necessary" $ do let (Source pkg _) = makeDummySource' ["test"] (Module [] Nothing) [] - files = compilePackage pkg + files = M.compilePackage pkg directoryStructure = [ "src-py2" "test" "__init__.py" , "src-py2" "test" "foo" "__init__.py" @@ -51,7 +55,7 @@ spec = do it "generates renamed package dirs if renames are configured" $ do let (Source pkg _) = makeDummySource' [] (Module [] Nothing) [(["foo"], ["quz"])] - files = compilePackage pkg + files = M.compilePackage pkg directoryStructure = [ "src-py2" "quz" "__init__.py" , "src-py2" "quz" "bar" "__init__.py" @@ -65,7 +69,7 @@ spec = do M.keysSet files `shouldBe` directoryStructure let (Source pkg' _) = makeDummySource' [] (Module [] Nothing) [(["foo", "bar"], ["bar"])] - files' = compilePackage pkg' + files' = M.compilePackage pkg' directoryStructure' = [ "src-py2" "foo" "__init__.py" , "src-py2" "bar" "__init__.py" @@ -90,3 +94,43 @@ spec = do parseModulePath "foo..bar" `shouldBe` Nothing parseModulePath "foo.bar>" `shouldBe` Nothing parseModulePath "foo.bar-" `shouldBe` Nothing + + describe "@numeric-constraints" $ do + it "fails if unsupported arguments are present" $ do + let Right annots = fromList + [ Annotation + "numeric-constraints" + [("min", Integer 1), ("unsupported", Integer 2)] + ] + compareErrorMessage annots + + it "fails if unsupported arguments type is given" $ do + let Right annots = fromList + [ Annotation + "numeric-constraints" + [("min", Integer 1), ("max", Text "2")] + ] + compareErrorMessage annots + + it "success" $ do + let Right annots = fromList + [ Annotation + "numeric-constraints" + [("min", Integer 1), ("max", Integer 2)] + ] + let Just result = getResult annots + isRight result `shouldBe` True + where + compareErrorMessage annots = do + let Just result = getResult annots + let Left errorMessage = result + errorMessage `shouldBe` + "Unsupported arguments on @numeric-constraints" + + getResult annots = + let + unboxed = TypeDeclaration "foo" (UnboxedType "int32") annots + (Source pkg _) = makeDummySource $ Module [unboxed] Nothing + files = M.compilePackage pkg + in + M.lookup ("src" "foo" "__init__.py") files diff --git a/test/nirum_fixture/fixture/constraints.nrm b/test/nirum_fixture/fixture/constraints.nrm new file mode 100644 index 0000000..35eab02 --- /dev/null +++ b/test/nirum_fixture/fixture/constraints.nrm @@ -0,0 +1,2 @@ +@numeric-constraints(min=1, max=12) +unboxed month (int32); diff --git a/test/python/constraints_test.py b/test/python/constraints_test.py new file mode 100644 index 0000000..e86ab68 --- /dev/null +++ b/test/python/constraints_test.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from pytest import raises + +from fixture.constraints import Month + + +def test_numeric_constraints(): + month = Month(1) + assert month.value == 1 + + with raises(ValueError): + Month(0) + + with raises(ValueError): + Month(13) diff --git a/test/python/setup_test.py b/test/python/setup_test.py index d89efa8..bacb269 100644 --- a/test/python/setup_test.py +++ b/test/python/setup_test.py @@ -22,7 +22,7 @@ def test_setup_metadata(): assert set(pkg['Provides']) == { 'fixture', 'fixture.foo', 'fixture.foo.bar', 'fixture.qux', 'fixture.reserved_keyword_enum', 'fixture.reserved_keyword_union', - 'fixture.types', 'fixture.alias', + 'fixture.types', 'fixture.alias', 'fixture.constraints', 'renamed', 'renamed.foo', 'renamed.foo.bar', 'fixture.datetime', 'fixture.name', @@ -46,6 +46,7 @@ def test_module_entry_points(): 'fixture.reserved-keyword-enum', 'fixture.reserved-keyword-union', 'fixture.types', 'fixture.alias', + 'fixture.constraints', 'renames.test.foo', 'renames.test.foo.bar', 'fixture.datetime', 'fixture.name',