yamlscript (empty) → 0.2.1.0
raw patch · 7 files changed
+332/−0 lines, 7 filesdep +aesondep +aeson-prettydep +base
Dependencies added: aeson, aeson-pretty, base, bytestring, hspec, text, unix, vector, yamlscript
Files
- License +21/−0
- lib/YAMLScript.hs +56/−0
- lib/YAMLScript/FFI.hs +61/−0
- test/Main.hs +21/−0
- test/Test.hs +39/−0
- test/YAMLScript/Tests.hs +64/−0
- yamlscript.cabal +70/−0
+ License view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2022-2025 Ingy döt Net <ingy@ingy.net> and contributors++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ lib/YAMLScript.hs view
@@ -0,0 +1,56 @@+-- Copyright 2022-2025 Ingy döt Net+-- This code is licensed under MIT license (See License for details)++{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE OverloadedStrings #-}++module YAMLScript+ ( loadYAMLScript+ , loadYAMLScriptFile+ , YAMLScriptError(..)+ ) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import Control.Exception (Exception, throwIO)+import Control.Monad.IO.Class (MonadIO, liftIO)+import System.IO.Unsafe (unsafePerformIO)+import YAMLScript.FFI++-- | Error type for YAMLScript operations+data YAMLScriptError+ = YAMLScriptParseError String+ | YAMLScriptRuntimeError String+ | YAMLScriptFFIError String+ deriving (Show, Eq)++instance Exception YAMLScriptError++-- | Load and evaluate YAMLScript code from a string+-- Returns the result as a JSON Value+loadYAMLScript :: MonadIO m => T.Text -> m Aeson.Value+loadYAMLScript input = liftIO $ do+ let inputBS = TE.encodeUtf8 input+ result <- loadYsToJsonFFI inputBS+ case Aeson.eitherDecode (LBS.fromStrict result) of+ Left err -> throwIO $ YAMLScriptParseError err+ Right value -> return value++-- | Load and evaluate YAMLScript code from a file+-- Returns the result as a JSON Value+loadYAMLScriptFile :: MonadIO m => FilePath -> m Aeson.Value+loadYAMLScriptFile filepath = liftIO $ do+ content <- BS.readFile filepath+ result <- loadYsToJsonFFI content+ case Aeson.eitherDecode (LBS.fromStrict result) of+ Left err -> throwIO $ YAMLScriptParseError err+ Right value -> return value++-- | Convenience function for pure contexts+-- Note: This uses unsafePerformIO and should be used carefully+loadYAMLScriptPure :: T.Text -> Aeson.Value+loadYAMLScriptPure = unsafePerformIO . loadYAMLScript
+ lib/YAMLScript/FFI.hs view
@@ -0,0 +1,61 @@+-- Copyright 2022-2025 Ingy döt Net+-- This code is licensed under MIT license (See License for details)++{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE CApiFFI #-}+{-# LANGUAGE ScopedTypeVariables #-}++module YAMLScript.FFI+ ( loadYsToJsonFFI+ ) where++import Foreign+import Foreign.C.String+import Foreign.C.Types+import qualified Data.ByteString as BS+import qualified Data.ByteString.Unsafe as BSU+import Control.Exception (bracket)++-- | GraalVM isolate thread type+type GraalIsolateThread = Ptr ()++-- | Foreign function interface to GraalVM isolate management+foreign import capi "graal_isolate.h graal_create_isolate"+ c_graal_create_isolate :: Ptr () -> Ptr () -> Ptr GraalIsolateThread -> IO CInt++foreign import capi "graal_isolate.h graal_tear_down_isolate"+ c_graal_tear_down_isolate :: GraalIsolateThread -> IO CInt++-- | Foreign function interface to load_ys_to_json+foreign import capi "libys.0.2.1.h load_ys_to_json"+ c_load_ys_to_json :: GraalIsolateThread -> CString -> IO CString++-- | Create a GraalVM isolate and run an action with it+withGraalIsolate :: (GraalIsolateThread -> IO a) -> IO a+withGraalIsolate action =+ alloca $ \(isolateThreadPtr :: Ptr GraalIsolateThread) -> do+ -- Create the isolate (following Python binding pattern: None, None, &isolatethread)+ rc <- c_graal_create_isolate nullPtr nullPtr isolateThreadPtr+ if rc /= 0+ then error $ "Failed to create GraalVM isolate (code " ++ show rc ++ ")"+ else do+ isolateThread <- peek isolateThreadPtr+ -- Run the action and ensure cleanup+ bracket+ (return isolateThread)+ (\thread -> do+ rc' <- c_graal_tear_down_isolate thread+ if rc' /= 0+ then error $ "Failed to tear down GraalVM isolate (code " ++ show rc' ++ ")"+ else return ())+ action++-- | Load YAMLScript code and return JSON result+loadYsToJsonFFI :: BS.ByteString -> IO BS.ByteString+loadYsToJsonFFI input =+ withGraalIsolate $ \isolateThread ->+ BSU.unsafeUseAsCString input $ \cInput -> do+ cResult <- c_load_ys_to_json isolateThread cInput+ if cResult == nullPtr+ then return BS.empty+ else BS.packCString cResult
+ test/Main.hs view
@@ -0,0 +1,21 @@+-- Copyright 2022-2025 Ingy döt Net+-- This code is licensed under MIT license (See License for details)++module Main where++import qualified Data.Text as T+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Encode.Pretty as Aeson+import qualified Data.ByteString.Lazy.Char8 as LBS+import System.Environment (getArgs)+import YAMLScript++main :: IO ()+main = do+ args <- getArgs+ case args of+ [] -> putStrLn "Usage: yamlscript-test <yamlscript-code>"+ (code:_) -> do+ let input = T.pack code+ result <- loadYAMLScript input+ LBS.putStrLn $ Aeson.encodePretty result
+ test/Test.hs view
@@ -0,0 +1,39 @@+-- Copyright 2022-2025 Ingy döt Net+-- This code is licensed under MIT license (See License for details)++{-# LANGUAGE OverloadedStrings #-}++module Main where++import Test.Hspec+import qualified Data.Text as T+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import YAMLScript+import YAMLScript.Tests++main :: IO ()+main = hspec $ do+ describe "YAMLScript" $ do+ it "loads simple YAML" $ do+ result <- loadYAMLScript "foo: bar"+ result `shouldBe` Aeson.object+ [("data", Aeson.object [("foo", Aeson.String "bar")])]++ it "evaluates YAMLScript function call" $ do+ result <- loadYAMLScript "!yamlscript/v0\nadd(40, 2)"+ result `shouldBe` Aeson.object [("data", Aeson.Number 42)]++ it "loads from file" $ do+ result <- loadYAMLScriptFile "test/data/simple.ys"+ result `shouldBe` Aeson.object+ [("data", Aeson.object [("test", Aeson.String "value")])]++ it "handles errors gracefully" $ do+ result <- loadYAMLScript "!yamlscript/v0\ninvalid: syntax"+ case result of+ Aeson.Object obj -> case KeyMap.lookup (Key.fromString "error") obj of+ Just _ -> return () -- Error present, test passes+ Nothing -> expectationFailure "Expected error response"+ _ -> expectationFailure "Expected JSON object"
+ test/YAMLScript/Tests.hs view
@@ -0,0 +1,64 @@+-- Copyright 2022-2025 Ingy döt Net+-- This code is licensed under MIT license (See License for details)++{-# LANGUAGE OverloadedStrings #-}++module YAMLScript.Tests where++import Test.Hspec+import qualified Data.Text as T+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.Vector as V+import YAMLScript++-- | Test basic YAML functionality+basicTests :: Spec+basicTests = describe "Basic YAML" $ do+ it "parses simple key-value pairs" $ do+ result <- loadYAMLScript "key: value"+ result `shouldBe` Aeson.object+ [("data", Aeson.object [("key", Aeson.String "value")])]++ it "parses lists" $ do+ result <- loadYAMLScript "list: [1, 2, 3]"+ result `shouldBe` Aeson.object+ [ ("data", Aeson.object+ [ ("list", Aeson.Array $ V.fromList+ [Aeson.Number 1, Aeson.Number 2, Aeson.Number 3])+ ])+ ]++ it "parses nested objects" $ do+ result <- loadYAMLScript "nested:\n key: value"+ result `shouldBe` Aeson.object+ [ ("data", Aeson.object+ [ ("nested", Aeson.object [("key", Aeson.String "value")])+ ])+ ]++-- | Test YAMLScript functionality+yamlscriptTests :: Spec+yamlscriptTests = describe "YAMLScript Features" $ do+ it "evaluates arithmetic expressions" $ do+ result <- loadYAMLScript "!yamlscript/v0\nadd(2, 3)"+ result `shouldBe` Aeson.object [("data", Aeson.Number 5)]++ it "evaluates function calls" $ do+ result <- loadYAMLScript "!yamlscript/v0\ninc(41)"+ result `shouldBe` Aeson.object [("data", Aeson.Number 42)]++-- | Test error handling+errorTests :: Spec+errorTests = describe "Error Handling" $ do+ it "returns error on invalid YAMLScript" $ do+ result <- loadYAMLScript "!yamlscript/v0\ninvalid: syntax"+ case result of+ Aeson.Object obj -> case KeyMap.lookup (Key.fromString "error") obj of+ Just _ -> return () -- Error present, test passes+ Nothing -> expectationFailure "Expected error response"+ _ -> expectationFailure "Expected JSON object"++ it "throws on missing file" $ do+ (loadYAMLScriptFile "nonexistent.ys") `shouldThrow` anyException
+ yamlscript.cabal view
@@ -0,0 +1,70 @@+cabal-version: 3.0+name: yamlscript+version: 0.2.1.0+synopsis: Haskell bindings for YAMLScript+description: Haskell bindings for YAMLScript, a functional+ programming language with YAML syntax.+ .+ YAMLScript allows you to add logic to your YAML files+ while maintaining compatibility with standard YAML.+license: MIT+license-file: License+author: Ingy döt Net <ingy@ingy.net>+maintainer: Ingy döt Net <ingy@ingy.net>+category: Data, Text, YAML+homepage: https://yamlscript.org+bug-reports: https://github.com/yaml/yamlscript/issues+tested-with: GHC==9.4.7, GHC==9.6.3, GHC==9.8.1, GHC==9.12.1++library+ exposed-modules: YAMLScript+ other-modules: YAMLScript.FFI+ build-depends: base >=4.17.0.0 && <5,+ bytestring ^>=0.11.0.0,+ text >=2.0.0.0 && <3,+ aeson >=2.0.0.0 && <3,+ unix >=2.7.0.0 && <3+ hs-source-dirs: lib+ default-language: Haskell2010+ default-extensions: ForeignFunctionInterface+ ghc-options: -Wall -Wcompat -Widentities+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wmissing-export-lists+ -Wmissing-home-modules+ -Wpartial-fields+ -Wredundant-constraints+ -- XXX Not sure what to do here. cabal doesn't like relative paths.+ -- Currently the Makefile copies the ../libys/lib to /tmp/libys/lib+ -- Feels like there should be a better way to do this.+ include-dirs: /tmp/libys/lib+ extra-libraries: ys+ extra-lib-dirs: /tmp/libys/lib++executable yamlscript-test+ main-is: Main.hs+ build-depends: base >=4.17.0.0 && <5,+ yamlscript,+ text >=2.0.0.0 && <3,+ aeson >=2.0.0.0 && <3,+ aeson-pretty >=0.8.0.0 && <1,+ bytestring ^>=0.11.0.0+ hs-source-dirs: test+ default-language: Haskell2010+ ghc-options: -Wall++test-suite yamlscript-tests+ type: exitcode-stdio-1.0+ main-is: Test.hs+ other-modules: YAMLScript.Tests+ build-depends: base >=4.17.0.0 && <5,+ yamlscript,+ hspec >=2.8.0.0 && <3,+ aeson >=2.0.0.0 && <3,+ text >=2.0.0.0 && <3,+ unix >=2.7.0.0 && <3,+ vector >=0.13.0.0 && <1+ hs-source-dirs: test+ default-language: Haskell2010+ ghc-options: -Wall+