packages feed

hydra-0.15.0: src/main/haskell/Hydra/Demos/AvroBicoder.hs

-- | Demo for the bidirectional Avro coder.
--
-- Exercises:
--   1. Forward: Avro schema + JSON data -> Hydra types + terms (with round-trip)
--   2. Reverse: Hydra types -> Avro schema -> JSON
--   3. Round-trip: Avro schema -> Hydra -> Avro schema (structural comparison)
--   4. Schema codec: .avsc file -> parse -> encode -> compare
--   5. Avro to property graph: Avro schema + JSON data -> GraphSON
module Hydra.Demos.AvroBicoder (
  -- * Top-level demo runners
  runAllDemos,
  runForwardDemo,
  runReverseDemo,
  runRoundTripDemo,
  runSchemaCodecDemo,
  runPropertyGraphDemo,
) where

import Hydra.Kernel
import qualified Hydra.Core as Core
import qualified Hydra.Avro.Schema as Avro
import qualified Hydra.Json.Model as Json
import qualified Hydra.Avro.Coder as AvroCoder
import qualified Hydra.Avro.Encoder as Encoder
import qualified Hydra.Avro.Environment as AvroEnv
import qualified Hydra.Avro.SchemaJson as SchemaJson
import qualified Hydra.Json.Parser as JsonParser
import qualified Hydra.Json.Writer as JsonWriter
import Hydra.Parsing (ParseResult(..), ParseSuccess(..), ParseError(..))
import qualified Hydra.Show.Errors as ShowError
import Hydra.Tools.AvroWorkflows (propertyGraphGraphsonLastMile, transformAvroJsonDirectory)
import qualified Hydra.Coders as Coders
import qualified Hydra.Util as Util
import qualified Hydra.Json.Writer as JsonWriter

import qualified Data.Map as M
import qualified Data.List as L
import System.Directory
import System.FilePath


-- | Run all demos in sequence
runAllDemos :: IO ()
runAllDemos = do
  putStrLn "============================================"
  putStrLn "Hydra Avro Bidirectional Coder Demo"
  putStrLn "============================================"
  putStrLn ""

  runForwardDemo
  putStrLn ""
  runReverseDemo
  putStrLn ""
  runRoundTripDemo
  putStrLn ""
  runSchemaCodecDemo
  putStrLn ""
  runPropertyGraphDemo

  putStrLn ""
  putStrLn "============================================"
  putStrLn "All demos completed successfully."
  putStrLn "============================================"


-- | Demo 1: Forward pipeline (Avro schema + JSON -> Hydra type + term)
--
-- Reads a real .avsc schema and example JSON data, converts to Hydra types and terms
-- using the forward adapter, then displays the results.
runForwardDemo :: IO ()
runForwardDemo = do
  putStrLn "--- Demo 1: Forward pipeline (Avro -> Hydra) ---"
  pwd <- getCurrentDirectory
  let cx = emptyContext

  -- Load the Review schema
  let schemaPath = combine pwd "src/test/avro/moviedemo/Review.avsc"
  schemaStr <- readFile schemaPath
  case parseJson schemaStr of
    Left e -> putStrLn $ "  Parse error: " ++ e
    Right schemaJson -> case SchemaJson.decodeSchema cx schemaJson of
      Left e -> putStrLn $ "  Schema decode error: " ++ show e
      Right avroSchema -> do
        putStrLn "  Loaded: Review.avsc"
        case avroSchema of
          Avro.SchemaNamed named -> do
            putStrLn $ "  Schema name: " ++ Avro.namedName named
            putStrLn $ "  Schema namespace: " ++ show (Avro.namedNamespace named)
          _ -> return ()

        -- Create forward adapter
        case AvroCoder.avroHydraAdapter cx avroSchema AvroCoder.emptyAvroEnvironment of
          Left e -> putStrLn $ "  Adapter error: " ++ show e
          Right (adapter, _env) -> do
            putStrLn $ "  Forward adapter created (lossy: " ++ show (Coders.adapterIsLossy adapter) ++ ")"

            -- Load and convert example data
            let dataPath = combine pwd "src/test/json/moviedemo/exampleReview.json"
            dataStr <- readFile dataPath
            case parseJson dataStr of
              Left e -> putStrLn $ "  Data parse error: " ++ e
              Right dataJson -> do
                case Coders.coderEncode (Coders.adapterCoder adapter) cx dataJson of
                  Left e -> putStrLn $ "  Encode error: " ++ show e
                  Right hydraTerm -> do
                    putStrLn "  JSON data -> Hydra term: OK"
                    -- Decode back to JSON to verify round-trip
                    case Coders.coderDecode (Coders.adapterCoder adapter) cx hydraTerm of
                      Left e -> putStrLn $ "  Decode error: " ++ show e
                      Right jsonResult -> do
                        let resultStr = JsonWriter.printJson jsonResult
                        putStrLn $ "  Term -> JSON round-trip: OK (" ++ show (length resultStr) ++ " chars)"
                        -- Write output
                        let outDir = "/tmp/hydra-avro-demo/forward"
                        createDirectoryIfMissing True outDir
                        writeFile (combine outDir "review-roundtrip.json") resultStr
                        putStrLn $ "  Output: " ++ combine outDir "review-roundtrip.json"


-- | Demo 2: Reverse pipeline (Hydra types -> Avro schema)
--
-- Defines Hydra types programmatically and encodes them as Avro schemas,
-- then serializes the schemas to JSON.
runReverseDemo :: IO ()
runReverseDemo = do
  putStrLn "--- Demo 2: Reverse pipeline (Hydra -> Avro schema) ---"
  let cx = emptyContext

  -- Define a small type graph: Person with Address
  let addressName = Core.Name "com.example.Address"
  let personName = Core.Name "com.example.Person"
  let addressType = Core.TypeRecord [
        Core.FieldType (Core.Name "street") (Core.TypeLiteral (Core.LiteralTypeString)),
        Core.FieldType (Core.Name "city") (Core.TypeLiteral (Core.LiteralTypeString)),
        Core.FieldType (Core.Name "zipCode") (Core.TypeLiteral (Core.LiteralTypeString))]
  let personType = Core.TypeRecord [
        Core.FieldType (Core.Name "name") (Core.TypeLiteral (Core.LiteralTypeString)),
        Core.FieldType (Core.Name "age") (Core.TypeLiteral (Core.LiteralTypeInteger Core.IntegerTypeInt32)),
        Core.FieldType (Core.Name "email") (Core.TypeMaybe (Core.TypeLiteral (Core.LiteralTypeString))),
        Core.FieldType (Core.Name "address") (Core.TypeVariable addressName),
        Core.FieldType (Core.Name "tags") (Core.TypeList (Core.TypeLiteral (Core.LiteralTypeString)))]
  let typeMap = M.fromList [(addressName, addressType), (personName, personType)]

  case Encoder.encodeType cx typeMap personName of
    Left err -> putStrLn $ "  ERROR: " ++ ShowError.error err
    Right adapter -> do
      let avroSchema = Coders.adapterTarget adapter
      let json = SchemaJson.encodeSchema avroSchema
      let jsonStr = JsonWriter.printJson json
      putStrLn "  Hydra type 'com.example.Person' encoded as Avro schema:"
      putStrLn $ "  " ++ jsonStr
      -- Write to file
      let outDir = "/tmp/hydra-avro-demo/reverse"
      createDirectoryIfMissing True outDir
      let outFile = combine outDir "Person.avsc"
      writeFile outFile jsonStr
      putStrLn $ "  Written to: " ++ outFile

      -- Also encode a term and show the JSON
      let personTerm = Core.TermRecord $ Core.Record personName [
            Core.Field (Core.Name "name") (Core.TermLiteral (Core.LiteralString "Alice Smith")),
            Core.Field (Core.Name "age") (Core.TermLiteral (Core.LiteralInteger (Core.IntegerValueInt32 30))),
            Core.Field (Core.Name "email") (Core.TermMaybe (Just (Core.TermLiteral (Core.LiteralString "alice@example.com")))),
            Core.Field (Core.Name "address") (Core.TermRecord $ Core.Record addressName [
              Core.Field (Core.Name "street") (Core.TermLiteral (Core.LiteralString "123 Main St")),
              Core.Field (Core.Name "city") (Core.TermLiteral (Core.LiteralString "Springfield")),
              Core.Field (Core.Name "zipCode") (Core.TermLiteral (Core.LiteralString "62701"))]),
            Core.Field (Core.Name "tags") (Core.TermList [
              Core.TermLiteral (Core.LiteralString "engineer"),
              Core.TermLiteral (Core.LiteralString "haskell")])]
      case Coders.coderEncode (Coders.adapterCoder adapter) cx personTerm of
        Left err -> putStrLn $ "  Term encode ERROR: " ++ ShowError.error err
        Right jsonVal -> do
          let termJsonStr = JsonWriter.printJson jsonVal
          putStrLn $ "  Term encoded as JSON: " ++ termJsonStr
          let termFile = combine outDir "person-data.json"
          writeFile termFile termJsonStr
          putStrLn $ "  Written to: " ++ termFile


-- | Demo 3: Round-trip (Avro -> Hydra -> Avro)
--
-- Parses a real .avsc schema, converts to Hydra types via the forward adapter,
-- then converts back to an Avro schema via the reverse encoder, and compares.
runRoundTripDemo :: IO ()
runRoundTripDemo = do
  putStrLn "--- Demo 3: Round-trip (Avro -> Hydra -> Avro) ---"
  pwd <- getCurrentDirectory
  let cx = emptyContext

  -- Load the AirplaneInfo schema
  let schemaPath = combine pwd "src/test/avro/aviationdemo/AirplaneInfo.avsc"
  schemaStr <- readFile schemaPath
  case parseJson schemaStr of
    Left e -> putStrLn $ "  Parse error: " ++ e
    Right schemaJson -> case SchemaJson.decodeSchema cx schemaJson of
      Left e -> putStrLn $ "  Schema decode error: " ++ show e
      Right avroSchema -> do
        putStrLn $ "  Loaded: AirplaneInfo.avsc"

        -- Forward: Avro -> Hydra
        case AvroCoder.avroHydraAdapter cx avroSchema AvroCoder.emptyAvroEnvironment of
          Left e -> putStrLn $ "  Forward adapter error: " ++ show e
          Right (fwdAdapter, _env) -> do
            let hydraType = Coders.adapterTarget fwdAdapter
            putStrLn $ "  Forward: Avro schema -> Hydra type (ok)"

            -- Reverse: Hydra -> Avro
            let typeName = Core.Name "com.example.AirplaneInfo"
            case Encoder.encodeType cx (M.singleton typeName hydraType) typeName of
              Left e -> putStrLn $ "  Reverse adapter error: " ++ show e
              Right revAdapter -> do
                let revSchema = Coders.adapterTarget revAdapter
                putStrLn $ "  Reverse: Hydra type -> Avro schema (ok)"

                -- Compare: serialize both and check structure
                let origJson = SchemaJson.encodeSchema avroSchema
                let revJson = SchemaJson.encodeSchema revSchema
                let origStr = JsonWriter.printJson origJson
                let revStr = JsonWriter.printJson revJson

                -- Write both for manual inspection
                let outDir = "/tmp/hydra-avro-demo/roundtrip"
                createDirectoryIfMissing True outDir
                writeFile (combine outDir "original.json") origStr
                writeFile (combine outDir "roundtripped.json") revStr
                putStrLn $ "  Original schema:     " ++ show (length origStr) ++ " chars"
                putStrLn $ "  Round-tripped schema: " ++ show (length revStr) ++ " chars"
                putStrLn $ "  Output written to: " ++ outDir

                -- Structural check: both should be named record schemas
                case (avroSchema, revSchema) of
                  (Avro.SchemaNamed origNamed, Avro.SchemaNamed revNamed) -> do
                    putStrLn $ "  Original name: " ++ Avro.namedName origNamed
                    putStrLn $ "  Round-trip name: " ++ Avro.namedName revNamed
                    let origFieldCount = case Avro.namedType origNamed of
                          Avro.NamedTypeRecord (Avro.Record fs) -> length fs
                          _ -> 0
                    let revFieldCount = case Avro.namedType revNamed of
                          Avro.NamedTypeRecord (Avro.Record fs) -> length fs
                          _ -> 0
                    putStrLn $ "  Field count: " ++ show origFieldCount ++ " -> " ++ show revFieldCount
                    if origFieldCount == revFieldCount
                      then putStrLn "  PASS: field counts match"
                      else putStrLn "  NOTE: field counts differ (expected if annotations affect structure)"
                  _ -> putStrLn "  WARNING: schemas have different top-level structure"


-- | Demo 4: Schema string codec
--
-- Demonstrates the avroSchemaStringCoder: parse JSON string -> Schema -> JSON string
runSchemaCodecDemo :: IO ()
runSchemaCodecDemo = do
  putStrLn "--- Demo 4: Schema string codec ---"
  let cx = emptyContext
  let coder = SchemaJson.avroSchemaStringCoder cx

  -- Encode a schema to a JSON string
  let schema = Avro.SchemaNamed $ Avro.Named {
        Avro.namedName = "Greeting",
        Avro.namedNamespace = Just "com.example",
        Avro.namedAliases = Nothing,
        Avro.namedDoc = Just "A simple greeting message",
        Avro.namedType = Avro.NamedTypeRecord $ Avro.Record [
          Avro.Field "message" (Just "The greeting text") (Avro.SchemaPrimitive Avro.PrimitiveString) Nothing Nothing Nothing M.empty,
          Avro.Field "timestamp" Nothing (Avro.SchemaPrimitive Avro.PrimitiveLong) Nothing Nothing Nothing M.empty],
        Avro.namedAnnotations = M.empty}

  case Coders.coderEncode coder cx schema of
    Left e -> putStrLn $ "  Encode error: " ++ show e
    Right jsonStr -> do
      putStrLn $ "  Encoded: " ++ jsonStr
      -- Decode it back
      case Coders.coderDecode coder cx jsonStr of
        Left e -> putStrLn $ "  Decode error: " ++ show e
        Right decoded -> do
          if decoded == schema
            then putStrLn "  PASS: round-trip produces identical schema"
            else putStrLn "  NOTE: round-trip produces structurally different schema (may differ in optional fields)"
          -- Re-encode to verify
          case Coders.coderEncode coder cx decoded of
            Left e -> putStrLn $ "  Re-encode error: " ++ show e
            Right jsonStr2 -> do
              if jsonStr == jsonStr2
                then putStrLn "  PASS: JSON string round-trips exactly"
                else do
                  putStrLn "  NOTE: JSON strings differ after re-encoding"
                  putStrLn $ "  First:  " ++ jsonStr
                  putStrLn $ "  Second: " ++ jsonStr2


-- | Demo 5: Avro to property graph (GraphSON)
--
-- Reads an Avro schema and JSON data, converts through the forward adapter,
-- extracts elements from @primaryKey annotations, builds a graph, then
-- transforms to a GraphSON property graph.
runPropertyGraphDemo :: IO ()
runPropertyGraphDemo = do
  putStrLn "--- Demo 5: Avro to property graph (GraphSON) ---"
  pwd <- getCurrentDirectory
  let schemaPath = combine pwd "src/test/avro/moviedemo/Review.avsc"
  let dataDir = combine pwd "src/test/json/moviedemo"
  let outDir = "/tmp/hydra-avro-demo/graphson"
  createDirectoryIfMissing True outDir
  transformAvroJsonDirectory propertyGraphGraphsonLastMile schemaPath dataDir outDir
  let outFile = combine outDir "exampleReview.jsonl"
  exists <- doesFileExist outFile
  if exists
    then do
      content <- readFile outFile
      let lineCount = length (lines content)
      putStrLn $ "  Generated " ++ show lineCount ++ " GraphSON vertices"
      putStrLn $ "  Output: " ++ outFile
      putStrLn "  First 3 vertices:"
      mapM_ (\l -> putStrLn $ "    " ++ take 120 l ++ if length l > 120 then "..." else "") (take 3 $ lines content)
    else putStrLn "  ERROR: output file not generated"


-- Helpers

parseJson :: String -> Either String Json.Value
parseJson s = case JsonParser.parseJson s of
  ParseResultSuccess success -> Right (parseSuccessValue success)
  ParseResultFailure err -> Left (parseErrorMessage err)