1

i'm using this function and need to pass it an Aeson Value:

{ logLevel : vega.Debug }

this is supposed to refer to an enum in a javascript package that the binding doesn't export.

afaict i'm supposed to use Data.Aeson.QQ.Simple for this, but everything i try that compiles puts quotes around "vega.Debug", which i can't have.

[aesonQQ| { logLevel : "vega.Debug" } |]

what am i missing? is there a way to use encode for this?

user1441998
  • 459
  • 4
  • 14
  • 1
    `{ logLevel : vega.Debug }` is not a valid JSON, so it can't be written as an Aeson `Value`. Can you clarify what JSON you are really trying to put? – Sir4ur0n Oct 02 '20 at 16:52
  • @Sir4ur0n - oh really? huh! i find javascript so repugnant that i touch it as little as possible, so i barely know anything about it or JSON. i'm trying to set the `logLevel` option as described [here](https://github.com/vega/vega-embed#options). it refers to a type `Level` [here](https://vega.github.io/vega/docs/api/view/#view_logLevel), which refers to values like `vega.Debug`, defined in that enum the OP links. if i use a bare Int it works: `toJSON VOpt {logLevel = 2}` where `VOpt` just newtypes an Int and instances `ToJSON`. won't JSON let you put expressions as field values? if not, why? – user1441998 Oct 02 '20 at 17:22
  • If the binding does not expose those constants you could define them manually in your Haskell program, e.g. `Debug :: Int`, `Debug = 1`, and then use `{ logLevel : Debug }`. Would that work for your use case? – Artem Pelenitsyn Oct 02 '20 at 17:44
  • I meant `{ logLevel = Debug }` of course. – Artem Pelenitsyn Oct 02 '20 at 18:06
  • @ArtemPelenitsyn - that sounds like my `VOpt` hack, i'm trying to figure out how to do it "right" -- ie safe, clean, no repeating :) – user1441998 Oct 02 '20 at 18:34

1 Answers1

1

In general, Aeson Values represent JSON objects only, so they don’t support embedded JavaScript expressions, or any other extensions.

If this API only accepts Values, you’re stuck. I think the best solution is to just duplicate the integer value of vega.Debug and serialise that.

Otherwise, a straightforward solution is to make a modified version of toHtmlWith that accepts a more flexible input type, such as a string:

toHtmlWith' :: Maybe Text -> VegaLite -> Text
toHtmlWith' mopts vl =
  let spec = encodeToLazyText (fromVL vl)
      -- NB: Removed ‘encodeToLazyText’ call here.
      opts = maybe "" (\o -> "," <> o) mopts

  in TL.unlines
    [ "<!DOCTYPE html>"
    , "<html>"
    , "<head>"
      -- versions are fixed at vega 5, vega-lite 4
    , "  <script src=\"https://cdn.jsdelivr.net/npm/vega@5\"></script>"
    , "  <script src=\"https://cdn.jsdelivr.net/npm/vega-lite@4\"></script>"
    , "  <script src=\"https://cdn.jsdelivr.net/npm/vega-embed\"></script>"
    , "</head>"
    , "<body>"
    , "<div id=\"vis\"></div>"
    , "<script type=\"text/javascript\">"
    , "  var spec = " <> spec <> ";"
    , "  vegaEmbed(\'#vis\', spec" <> opts <> ").then(function(result) {"
    , "  // Access the Vega view instance (https://vega.github.io/vega/docs/api/view/) as result.view"
    , "  }).catch(console.error);"
    , "</script>"
    , "</body>"
    , "</html>"
    ]

Then you can call encodeToLazyText on your own Aeson values, or include arbitrary Text strings as needed.

If you really want to avoid duplicating the page contents, then you could also call the existing toHtmlWith with a Value containing a special delimiter that you control, such as String "<user1441998>vega.Debug</user1441998>", and then use that delimiter to postprocess the result:

unquoteHackSplices = replace "\"<user1441998>" ""
  . replace "</user1441998>\"" ""

is there a way to use encode for this?

As yet another hack, you could make a ToJSON instance for your type that implements toEncoding but not toJSON, and have the encoded value be a JavaScript expression (i.e. invalid JSON). You would want to make toJSON raise an error so you don’t use it inadvertently.


If you want to generate JavaScript code in general, I would have a look at language-javascript. Instead of producing a Value, produce a JSExpression and then use one of the pretty-printing functions like renderToText to render it. Here’s a sketch of the structure of a possible solution:

-- Like ‘ToJSON’ but may produce arbitrary JavaScript expressions
class ToJavaScript a where
  toJavaScript :: a -> JSExpression

-- Helper function to convert from Aeson Value
jsFromJson :: Value -> JSExpression
jsFromJson v = case v of
  Object o -> JSObjectLiteral …
  Array a -> JSArrayLiteral …
  String s -> JSStringLiteral …
  …

instance ToJavaScript YourType where
  toJavaScript = …

rendered :: Text
rendered = renderToText
  $ JSAstExpression (toJavaScript yourValue) JSNoAnnot

Your expression would have the form:

JSMemberDot
  (JSIdentifier JSNoAnnot "vega")
  JSNoAnnot
  (JSIdentifier JSNoAnnot "Debug")

The JSAnnot type would also allow you to include comments in the generated result. Bear in mind that the language-javascript pretty-printing is likely less well optimised than Aeson’s JSON serialisation.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • thanks for the super thorough answer! the `toEncoding` idea seems small and clean, any reason you downplay it? :) – user1441998 Oct 02 '20 at 20:00
  • 1
    @user1441998: I don’t know if it’ll work, mainly, since I don’t know if there are cases that go through `toJSON` first when they should go through `toEncoding`. Also it does break what I would *expect* to be invariants of `ToJSON`, namely that going through `toEncoding` and `encode . toJSON` should produce the same end result. – Jon Purdy Oct 02 '20 at 20:16