diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 890c30b..23111de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,18 +179,6 @@ jobs: zip -r ../../query-json-windows-x64.zip . cd ../.. - - name: End to end Test (Linux) - if: ${{ matrix.os == 'ubuntu-latest' }} - run: npx bats ./e2e/test.sh - env: - IS_CI: true - - - name: End to end Test (Windows) - if: ${{ matrix.os == 'windows-latest' }} - run: sh -c 'npx bats ./e2e/test.sh' \"$(pwd)/node_modules/bats/bin/bats\" - env: - IS_CI: true - - name: Check if should be published if: ${{ success() && github.event_name != 'pull_request' }} id: newVersion diff --git a/bin/Bin.re b/bin/Bin.re index 7f6beae..96f12b0 100644 --- a/bin/Bin.re +++ b/bin/Bin.re @@ -1,30 +1,32 @@ open QueryJsonCore; open QueryJsonCore.Console; -type inputKind = - | File - | Inline; +module Runtime = { + type inputKind = + | File + | Inline; -let run = (~kind, ~payload, ~noColor, runtime) => { - let input = - switch (kind, payload) { - | (File, Some(file)) => Json.parseFile(file) - | (Inline, Some(str)) => Json.parseString(str) - | (_, None) => - let ic = Unix.(stdin |> in_channel_of_descr); - Json.parseChannel(ic); - }; + let run = (~kind, ~payload, ~noColor, runtime) => { + let input = + switch (kind, payload) { + | (File, Some(file)) => Json.parseFile(file) + | (Inline, Some(str)) => Json.parseString(str) + | (_, None) => + let ic = Unix.(stdin |> in_channel_of_descr); + Json.parseChannel(ic); + }; - switch (input) { - | Ok(json) => - switch (runtime(json)) { + switch (input) { | Ok(json) => - json - |> List.map(Json.toString(~colorize=!noColor, ~summarize=false)) - |> List.iter(print_endline) - | Error(err) => print_endline(Errors.printError(err)) - } - | Error(err) => print_endline(Errors.printError(err)) + switch (runtime(json)) { + | Ok(json) => + json + |> List.map(Json.toString(~colorize=!noColor, ~summarize=false)) + |> List.iter(print_endline) + | Error(err) => print_endline(Console.Errors.printError(err)) + } + | Error(err) => print_endline(Console.Errors.printError(err)) + }; }; }; @@ -32,16 +34,16 @@ let execution = ( query: option(string), payload: option(string), - kind: inputKind, - _verbose: bool, + kind: Runtime.inputKind, + verbose: bool, debug: bool, noColor: bool, ) => { switch (query) { | Some(q) => - Main.parse(~debug, q) + Main.parse(~debug, ~verbose, q) |> Result.map(Compiler.compile) - |> Result.iter(run(~payload, ~kind, ~noColor)) + |> Result.iter(Runtime.run(~payload, ~kind, ~noColor)) | None => print_endline(usage()) }; }; @@ -60,8 +62,12 @@ let json = { let kind = { let doc = "input kind"; - let kindEnum = Arg.enum([("file", File), ("inline", Inline)]); - Arg.(value & opt(kindEnum, ~vopt=File, File) & info(["k", "kind"], ~doc)); + let kindEnum = Arg.enum([("file", Runtime.File), ("inline", Inline)]); + Arg.( + value + & opt(kindEnum, ~vopt=Runtime.File, File) + & info(["k", "kind"], ~doc) + ); }; let verbose = { diff --git a/dune-project b/dune-project index fe69753..b9f3ba6 100644 --- a/dune-project +++ b/dune-project @@ -4,6 +4,8 @@ (generate_opam_files true) +(cram enable) + (license MIT) (maintainers "David Sancho ") diff --git a/e2e/test.sh b/e2e/test.sh deleted file mode 100755 index 0463b83..0000000 --- a/e2e/test.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bats - -function query-json () { - if [[ -n $IS_CI ]]; then - echo "Running in CI mode" - run ./query-json "$@" - else - chmod +x _build/default/bin/Bin.exe - run "_build/default/bin/Bin.exe" "$@" - fi -} - -@test "json call works ok" { - query-json --no-color '.first.name' e2e/mock.json - [ "$status" -eq 0 ] - [ "$output" = '"John Doe"' ] -} - -@test "inline call works ok" { - query-json --no-color --kind=inline '.' '{ "a": 1 }' - [ "$status" -eq 0 ] - [ "$output" = '{ "a": 1 }' ] -} - -# Can't test stdin on bash :( -# @test "stdin works ok" { -# cat e2e/mock.json | query-json '.first.name' -# [ "$status" -eq 0 ] -# [ "$output" = '"John Doe"' ] -# } - -@test "non defined field gives back null" { - query-json --no-color '.wat?' e2e/mock.json - [ "$status" -eq 0 ] - [ "$output" = "null" ] -} - -teardown () { - echo "$BATS_TEST_NAME - --------- -$output --------- - -" -} diff --git a/source/Ast.re b/source/Ast.re index ed8d42b..d0905f9 100644 --- a/source/Ast.re +++ b/source/Ast.re @@ -1,14 +1,14 @@ -[@deriving show] +[@deriving show({with_path: false})] type regex = string; -[@deriving show] +[@deriving show({with_path: false})] type literal = | Bool(bool) /* true */ | String(string) /* "TEXT" */ | Number(float) /* 123 or 123.0 */ | Null; /* null */ -[@deriving show] +[@deriving show({with_path: false})] type expression = | Identity /* . */ | Empty /* empty */ @@ -47,6 +47,8 @@ type expression = | IsNan /* Array */ | Index(int) /* .[1] */ + | Iterator /* .[] */ + | IteratorWithRange(list(int)) /* .[] */ | Range(int, int) /* range(1, 10) */ | Flatten /* flatten */ | Head /* head */ diff --git a/source/Compiler.re b/source/Compiler.re index f8d7d97..3cf090c 100644 --- a/source/Compiler.re +++ b/source/Compiler.re @@ -230,10 +230,10 @@ let member = (key: string, opt: bool, json: Json.t) => { let index = (value: int, json: Json.t) => { switch (json) { + | `List(list) when List.length(list) > value => + Results.return(Json.index(value, json)) | `List(list) => - List.length(list) > value - ? Results.return(Json.index(value, json)) - : Error(makeAcessingToMissingItem(value, List.length(list))) + Error(makeAcessingToMissingItem(value, List.length(list))) | _ => Error(makeError("[" ++ string_of_int(value) ++ "]", json)) }; }; diff --git a/source/Console.re b/source/Console.re index bb41fba..fe8c77e 100644 --- a/source/Console.re +++ b/source/Console.re @@ -75,13 +75,10 @@ module Errors = { }; }; - let make = (~input, ~start: Lexing.position, ~end_: Lexing.position, exn) => { - let exnToString = extractExn(Printexc.to_string(exn)); + let make = (~input, ~start: Lexing.position, ~end_: Lexing.position) => { let pointerRange = String.make(end_.pos_cnum - start.pos_cnum, '^'); - Chalk.red(Chalk.bold(exnToString)) - ++ Formatting.enter(1) - ++ Formatting.indent(4) + Chalk.red(Chalk.bold("Parse error: ")) ++ "Problem parsing at position " ++ positionToString(start, end_) ++ Formatting.enter(2) diff --git a/source/Json.re b/source/Json.re index 303ef4d..afd55ec 100644 --- a/source/Json.re +++ b/source/Json.re @@ -53,6 +53,13 @@ let record = Easy_format.list; let id = (i: string): string => i; +let pp_float = float => + if (Stdlib.Float.equal(Stdlib.Float.round(float), float)) { + float |> int_of_float |> string_of_int; + } else { + Printf.sprintf("%g", float); + }; + module Color = { let rec format = (json: t) => switch (json) { @@ -62,7 +69,7 @@ module Color = { | `Int(i) => Easy_format.Atom(i |> string_of_int |> Chalk.green, Easy_format.atom) | `Float(f) => - Easy_format.Atom(f |> string_of_float |> Chalk.green, Easy_format.atom) + Easy_format.Atom(f |> pp_float |> Chalk.green, Easy_format.atom) | `String(s) => Easy_format.Atom(encode(s) |> quotes |> Chalk.green, Easy_format.atom) | `Intlit(s) => Easy_format.Atom(s |> Chalk.green, Easy_format.atom) @@ -95,7 +102,7 @@ module Summarize = { | `Bool(b) => Easy_format.Atom(string_of_bool(b), Easy_format.atom) | `Int(i) => Easy_format.Atom(i |> string_of_int, Easy_format.atom) | `Intlit(s) => Easy_format.Atom(s, Easy_format.atom) - | `Float(f) => Easy_format.Atom(f |> string_of_float, Easy_format.atom) + | `Float(f) => Easy_format.Atom(f |> pp_float, Easy_format.atom) | `String(s) => Easy_format.Atom(encode(s) |> quotes, Easy_format.atom) | `List([]) => Easy_format.Atom("[]", Easy_format.atom) | `Variant(s, _opt) => Easy_format.Atom(s, Easy_format.atom) @@ -123,7 +130,7 @@ module NoColor = { | `Bool(b) => Easy_format.Atom(string_of_bool(b), Easy_format.atom) | `Int(i) => Easy_format.Atom(i |> string_of_int, Easy_format.atom) | `Intlit(s) => Easy_format.Atom(s, Easy_format.atom) - | `Float(f) => Easy_format.Atom(f |> string_of_float, Easy_format.atom) + | `Float(f) => Easy_format.Atom(f |> pp_float, Easy_format.atom) | `String(s) => Easy_format.Atom(encode(s) |> quotes, Easy_format.atom) | `Variant(s, _opt) => Easy_format.Atom(encode(s) |> quotes, Easy_format.atom) diff --git a/source/Main.re b/source/Main.re index 1eede8d..7be0481 100644 --- a/source/Main.re +++ b/source/Main.re @@ -27,17 +27,18 @@ let provider = (token, start, stop); }; -let parse = (input: string, ~debug: bool): result(expression, string) => { +let parse = (input: string, ~debug, ~verbose): result(expression, string) => { let buf = Sedlexing.Utf8.from_string(input); let lexer = () => provider(~debug, buf); - if (debug) { - print_endline(""); - }; - - try(Ok(menhir(lexer))) { - | exn => + switch (menhir(lexer)) { + | ast => + if (verbose) { + print_endline(show_expression(ast)); + }; + Ok(ast); + | exception _exn => let Location.{loc_start, loc_end, _} = last_position^; - Error(make(~input, ~start=loc_start, ~end_=loc_end, exn)); + Error(make(~input, ~start=loc_start, ~end_=loc_end)); }; }; diff --git a/source/Parser.mly b/source/Parser.mly index 55ccd53..c1bdb5e 100644 --- a/source/Parser.mly +++ b/source/Parser.mly @@ -1,6 +1,6 @@ %{ - open Ast;; - open Console.Errors;; + open Ast + open Console.Errors %} %token STRING @@ -86,20 +86,20 @@ term: | RECURSE; { Recurse } | s = STRING; - { Literal(String(s)) } + { Literal (String s) } | n = NUMBER; - { Literal(Number(n)) } + { Literal (Number n) } | b = BOOL; - { Literal(Bool(b)) } + { Literal (Bool b) } | NULL - { Literal(Null) } + { Literal Null } | f = FUNCTION; from = NUMBER; SEMICOLON; upto = NUMBER; CLOSE_PARENT; { match f with | "range" -> Range(int_of_float(from), int_of_float(upto)) - | _ -> failwith(f ^ " is not a valid function") + | _ -> Error(f ^ " is not a valid function") } | f = FUNCTION; CLOSE_PARENT; - { failwith(f ^ "(), should contain a body") } + { Error(f ^ "(), should contain a body") } | f = FUNCTION; cb = expr; CLOSE_PARENT; { match f with | "filter" -> Map(Select(cb)) (* for backward compatibility *) @@ -124,17 +124,17 @@ term: | "split" -> Split(cb) | "join" -> Join(cb) | "contains" -> Contains(cb) - | "startswith" -> failwith(renamed f "starts_with") - | "endswith" -> failwith(renamed f "ends_with") - | _ -> failwith(missing f) + | "startswith" -> Error(renamed f "starts_with") + | "endswith" -> Error(renamed f "ends_with") + | _ -> Error(missing f) } | f = IDENTIFIER; { match f with | "empty" -> Empty - | "if" -> failwith(notImplemented f) - | "then" -> failwith(notImplemented f) - | "else" -> failwith(notImplemented f) - | "break" -> failwith(notImplemented f) + | "if" -> Error(notImplemented f) + | "then" -> Error(notImplemented f) + | "else" -> Error(notImplemented f) + | "break" -> Error(notImplemented f) | "keys" -> Keys | "flatten" -> Flatten | "head" -> Head @@ -164,21 +164,25 @@ term: | "nan" -> Nan | "is_nan" -> IsNan | "not" -> Not - | "isnan" -> failwith(renamed f "is_nan") - | "reduce" -> failwith(renamed f "reduce()") - | "tonumber" -> failwith(renamed f "to_number") - | "isinfinite" -> failwith(renamed f "is_infinite") - | "isfinite" -> failwith(renamed f "is_finite") - | "isnormal" -> failwith(renamed f "is_normal") - | "tostring" -> failwith(renamed f "to_string") - | _ -> failwith(missing f) + | "isnan" -> Error(renamed f "is_nan") + | "reduce" -> Error(renamed f "reduce()") + | "tonumber" -> Error(renamed f "to_number") + | "isinfinite" -> Error(renamed f "is_infinite") + | "isfinite" -> Error(renamed f "is_finite") + | "isnormal" -> Error(renamed f "is_normal") + | "tostring" -> Error(renamed f "to_string") + | _ -> Error(missing f) } - | OPEN_BRACKET; CLOSE_BRACKET; - { List(Empty) } + +/* | OPEN_BRACKET; CLOSE_BRACKET; + { List(Empty) } */ + | OPEN_BRACKET; e = expr; CLOSE_BRACKET; { List(e) } - | OPEN_PARENT; e = expr; CLOSE_PARENT; - { e } + + /* | OPEN_PARENT; e = expr; CLOSE_PARENT; + { e } */ + | e = term; OPEN_BRACKET; i = NUMBER; CLOSE_BRACKET { Pipe(e, Index(int_of_float(i))) } diff --git a/source/Tokenizer.re b/source/Tokenizer.re index bc04b51..17aa545 100644 --- a/source/Tokenizer.re +++ b/source/Tokenizer.re @@ -105,6 +105,6 @@ let rec tokenize = buf => { Ok(NUMBER(num)); | space => tokenize(buf) | any => Error("Unexpected character '" ++ lexeme(buf) ++ "'") - | _ => Error("Unexpected character") + | _ => Error("Unexpected character '" ++ lexeme(buf) ++ "'") }; }; diff --git a/source/dune b/source/dune index 89905a6..260da8d 100644 --- a/source/dune +++ b/source/dune @@ -8,5 +8,11 @@ (backend bisect_ppx))) (menhir - (flags --strict --external-tokens Tokenizer --explain --dump) + (flags + --strict + --external-tokens + Tokenizer + --explain + --dump + --unused-tokens) (modules Parser)) diff --git a/test/Parsing_test.re b/test/Parsing_test.re index 210daa9..d6400ab 100644 --- a/test/Parsing_test.re +++ b/test/Parsing_test.re @@ -4,11 +4,12 @@ let assert_string = (left, right) => Alcotest.check(Alcotest.string, "should be equal", right, left); let debug = false; +let verbose = false; let case = (input, expected) => { let fn = () => { let result = - switch (parse(~debug, input)) { + switch (parse(~debug, ~verbose, input)) { | Ok(r) => r | Error(err) => Alcotest.fail(err) }; @@ -21,6 +22,7 @@ let case = (input, expected) => { let tests = [ case(".[1]", Pipe(Identity, Index(1))), + /* case(".[]", Pipe(Identity, Index(1))), */ case("[1]", List(Literal(Number(1.)))), case(".store.books", Pipe(Key("store", false), Key("books", false))), case(".books[1]", Pipe(Key("books", false), Index(1))), diff --git a/test/_mapping.t b/test/_mapping.t new file mode 100644 index 0000000..7086c88 --- /dev/null +++ b/test/_mapping.t @@ -0,0 +1,20 @@ + $ Bin '.first.pages | .[]' mock.json + { + "id": 1, + "title": "The Art of Flipping Coins", + "url": "http://example.com/398eb027/1" + } + { + "id": 2, + "deleted": true + } + { + "id": 3, + "title": "Artichoke Salad", + "url": "http://example.com/398eb027/3" + } + { + "id": 4, + "title": "Flying Bananas", + "url": "http://example.com/398eb027/4" + } diff --git a/test/accessing.t b/test/accessing.t new file mode 100644 index 0000000..e5bd588 --- /dev/null +++ b/test/accessing.t @@ -0,0 +1,20 @@ + $ Bin --debug --verbose '' <<< '{}' + EOF + + $ Bin --debug --verbose '.' <<< '{ "flores": 12 }' + 12 + +$ Bin '23' <<< '{}' +23 + +$ Bin '.foo.bar' <<< '{ "foo": { "bar": 23 }}' +23 + +$ Bin '.foo.bar.baz?' <<< '{ "foo": { "bar": {} }}' +null + +$ Bin '.foo.bar.baz' <<< '{ "foo": { "bar": {} }}' + +Error: Trying to ".baz" on an object, that don't have the field "baz": +{} + diff --git a/test/dune b/test/dune index b229fe7..a9d6eee 100644 --- a/test/dune +++ b/test/dune @@ -1,3 +1,7 @@ -(test - (name test) - (libraries alcotest query-json.core fmt yojson ppx_deriving.runtime)) +; (test +; (name test) +; (libraries alcotest query-json.core fmt yojson ppx_deriving.runtime)) + +(cram + (package query-json) + (deps %{bin:Bin} ./mock.json)) diff --git a/test/e2e b/test/e2e new file mode 100644 index 0000000..bd81602 --- /dev/null +++ b/test/e2e @@ -0,0 +1,11 @@ +json call works + $ Bin --no-color '.first.name' mock.json + "John Doe" + +inline call works + $ Bin --no-color --kind=inline '.' '{ "a": 1 }' + { "a": 1 } + +non defined field gives back null + $ Bin --no-color '.wat?' mock.json + null diff --git a/test/ints b/test/ints new file mode 100644 index 0000000..d60c3aa --- /dev/null +++ b/test/ints @@ -0,0 +1,3 @@ +Prints ints properly + $ Bin '[1, 2, 3]' <<< [1,2,3] + [ 1, 2, 3 ] diff --git a/test/issue_36.t b/test/issue_36.t new file mode 100644 index 0000000..c2815c6 --- /dev/null +++ b/test/issue_36.t @@ -0,0 +1,20 @@ +Map works in jq + $ jq '.[]' <<< [1,2,3] + 1 + 2 + 3 + +Range access works in jq + $ jq '.[1, 2]' <<< [1,2,3] + 2 + 3 + +Map works +$ Bin --verbose '.[]' <<< [1,2,3] +1 +2 +3 + +Range access works in jq +$ Bin '.[1, 2]' <<< [1,2,3] + diff --git a/e2e/mock.json b/test/mock.json similarity index 100% rename from e2e/mock.json rename to test/mock.json