diff --git a/apps/backend/src/api/signatures.gleam b/apps/backend/src/api/signatures.gleam index 73789fb..ac01c9d 100644 --- a/apps/backend/src/api/signatures.gleam +++ b/apps/backend/src/api/signatures.gleam @@ -8,9 +8,12 @@ import backend/gleam/generate/types.{ constant_to_json, function_to_json, type_alias_to_json, type_definition_to_json, } +import backend/gleam/type_search/state as type_search import backend/postgres/queries import gleam/bool import gleam/dict +import gleam/erlang/process +import gleam/function import gleam/json import gleam/list import gleam/option.{None, Some} @@ -132,6 +135,7 @@ fn upsert_functions(ctx: Context, module: context.Module) { result.all({ use #(function_name, function) <- list.map(all_functions) use gen <- result.try(function_to_json(ctx, function_name, function)) + let signature = function_to_string(function_name, function) queries.upsert_package_type_fun_signature( db: ctx.db, name: function_name, @@ -139,13 +143,22 @@ fn upsert_functions(ctx: Context, module: context.Module) { documentation: function.documentation, metadata: Some(function.implementations) |> metadata.generate(function.deprecation, _), - signature: function_to_string(function_name, function), + signature: signature, json_signature: gen.0, parameters: gen.1, module_id: module.id, deprecation: function.deprecation, implementations: Some(function.implementations), ) + |> function.tap(fn(content) { + case ctx.type_search_subject, content { + option.Some(subject), Ok([id]) -> { + process.send(subject, type_search.Add(signature, id)) + content + } + _, _ -> content + } + }) }) } diff --git a/apps/backend/src/backend.gleam b/apps/backend/src/backend.gleam index a82204b..288fdf0 100644 --- a/apps/backend/src/backend.gleam +++ b/apps/backend/src/backend.gleam @@ -1,4 +1,5 @@ import backend/config +import backend/gleam/type_search/state as type_search import backend/postgres/postgres import backend/router import dot_env @@ -25,6 +26,15 @@ pub fn main() { logger.set_level(cnf.level) setup.radiate() + let assert Ok(subject) = type_search.init(ctx.db) + // let assert Ok(_) = + // supervisor.start(fn(children) { + // use _ <- function.tap(children) + // supervisor.add(children, { supervisor.worker(fn(_) { Ok(subject) }) }) + // }) + + let ctx = ctx |> config.add_type_search_subject(subject) + let assert Ok(_) = router.handle_request(_, ctx) |> wisp.mist_handler(secret_key_base) diff --git a/apps/backend/src/backend/config.gleam b/apps/backend/src/backend/config.gleam index 603f42e..cdcada2 100644 --- a/apps/backend/src/backend/config.gleam +++ b/apps/backend/src/backend/config.gleam @@ -1,5 +1,8 @@ +import backend/gleam/type_search/state as type_search import gleam/erlang/os +import gleam/erlang/process.{type Subject} import gleam/int +import gleam/option.{type Option} import gleam/pgo import gleam/result import wisp @@ -16,6 +19,7 @@ pub type Context { hex_api_key: String, github_token: String, env: Environment, + type_search_subject: Option(Subject(type_search.Msg)), ) } @@ -66,3 +70,7 @@ pub fn scaleway_keys() { let assert Ok(secret_key) = os.get_env("SCALEWAY_SECRET_KEY") #(access_key, secret_key) } + +pub fn add_type_search_subject(context, subject) { + Context(..context, type_search_subject: option.Some(subject)) +} diff --git a/apps/backend/src/backend/gleam/context.gleam b/apps/backend/src/backend/gleam/context.gleam index 1c1238e..146f567 100644 --- a/apps/backend/src/backend/gleam/context.gleam +++ b/apps/backend/src/backend/gleam/context.gleam @@ -1,4 +1,7 @@ +import backend/gleam/type_search/state as type_search import gleam/dict.{type Dict} +import gleam/erlang/process.{type Subject} +import gleam/option.{type Option} import gleam/package_interface import gleam/pgo import tom @@ -11,6 +14,7 @@ pub type Context { /// Allow to bypass parameters relations if activated. /// This allows to ignore internals for example. ignore_parameters_errors: Bool, + type_search_subject: Option(Subject(type_search.Msg)), ) } diff --git a/apps/backend/src/backend/gleam/parse.gleam b/apps/backend/src/backend/gleam/parse.gleam index cbfe266..7b4ca10 100644 --- a/apps/backend/src/backend/gleam/parse.gleam +++ b/apps/backend/src/backend/gleam/parse.gleam @@ -2,7 +2,6 @@ import chomp.{do, return} import chomp/lexer import chomp/span import gleam/dict.{type Dict} -import gleam/io import gleam/list import gleam/option.{None, Some} import gleam/pair @@ -14,23 +13,9 @@ pub type Kind { Index(String, Int) Custom(String, List(Kind)) Function(List(Kind), Kind) + Tuple(List(Kind)) } -pub fn main() { - "fn inner_text(plinth/browser/element.Element) -> String" - |> parse_function - |> io.debug -} - -// pub type SigKind(a) { -// Kind(Kind) -// Fn(a) -// } - -// pub type Signature { -// Signature(children: Dict(Kind(Signature), List(Signature)), rows: List(Int)) -// } - fn parse_qualified_name() { parse_upper_name() |> chomp.then(fn(content) { @@ -62,39 +47,67 @@ fn parse_name() { } } +fn parse_label() { + chomp.backtrackable({ + use name <- do( + chomp.take_map(fn(token) { + case token { + token.Name(content) -> Some(Index(content, 0)) + _ -> None + } + }), + ) + use _ <- do(chomp.token(token.Colon)) + return(name) + }) +} + fn parse_type_parameter() { use _ <- do(chomp.token(token.LeftParen)) - use content <- do( - chomp.one_of([parse_qualified_name(), parse_name()]) - |> chomp.sequence(chomp.token(token.Comma)), - ) + use content <- do(parse_kind() |> chomp.sequence(chomp.token(token.Comma))) use _ <- do(chomp.token(token.RightParen)) return(content) } fn parse_return() { use _ <- do(chomp.token(token.RightArrow)) - use content <- do(parse_upper_name()) + use content <- do(parse_kind()) return(content) } +fn parse_tuple() { + use _ <- do(chomp.token(token.Hash)) + use _ <- do(chomp.token(token.LeftParen)) + use content <- do(parse_kind() |> chomp.sequence(chomp.token(token.Comma))) + use _ <- do(chomp.token(token.RightParen)) + return(Tuple(content)) +} + fn parse_fn() { use _ <- do(chomp.token(token.Fn)) use _ <- do(chomp.optional(parse_name())) - use _ <- do(chomp.token(token.LeftParen)) + use _ <- do(chomp.optional(chomp.token(token.LeftParen))) use content <- do( - chomp.one_of([ - parse_fn(), - chomp.backtrackable(parse_qualified_name()), - parse_name(), - ]) + { + use _ <- do(chomp.optional(parse_label())) + parse_kind() + } |> chomp.sequence(chomp.token(token.Comma)), ) - use _ <- do(chomp.token(token.RightParen)) + use _ <- do(chomp.optional(chomp.token(token.RightParen))) use content_ <- do(parse_return()) return(Function(content, content_)) } +fn parse_kind() { + chomp.one_of([ + parse_fn(), + parse_tuple(), + chomp.backtrackable(parse_qualified_name()), + parse_name(), + ]) +} + pub fn parse_function(input: String) { let tokens = input @@ -141,5 +154,13 @@ fn replace_indexed( let #(return_value, accs) = replace_indexed(accs, return_value) #(Function(list.reverse(new_kinds), return_value), accs) } + Tuple(kinds) -> { + let #(new_kinds, accs) = + list.fold(kinds, #([], #(indexes, current)), fn(acc, val) { + let res = replace_indexed(acc.1, val) + #([res.0, ..acc.0], res.1) + }) + #(Tuple(list.reverse(new_kinds)), accs) + } } } diff --git a/apps/backend/src/backend/gleam/type_search.gleam b/apps/backend/src/backend/gleam/type_search.gleam index e7059c5..b557471 100644 --- a/apps/backend/src/backend/gleam/type_search.gleam +++ b/apps/backend/src/backend/gleam/type_search.gleam @@ -30,7 +30,13 @@ fn do_add(searches: TypeSearch, kinds: List(#(Kind, option.Option(Int)))) { [#(kind, option.Some(id))] -> dict.get(searches.entries, kind) |> result.unwrap(empty()) - |> fn(s) { TypeSearch(..s, rows: [id, ..s.rows]) } + |> fn(s: TypeSearch) { + let rows = case list.contains(s.rows, id) { + True -> s.rows + False -> [id, ..s.rows] + } + TypeSearch(..s, rows: rows) + } |> dict.insert(searches.entries, kind, _) |> fn(a) { TypeSearch(..searches, entries: a) } [#(kind, _), ..rest] -> diff --git a/apps/backend/src/backend/gleam/type_search/state.gleam b/apps/backend/src/backend/gleam/type_search/state.gleam new file mode 100644 index 0000000..aa32b8a --- /dev/null +++ b/apps/backend/src/backend/gleam/type_search/state.gleam @@ -0,0 +1,85 @@ +import backend/gleam/parse +import backend/gleam/type_search.{type TypeSearch} +import gleam/bool +import gleam/dynamic +import gleam/erlang/process.{type Subject} +import gleam/function +import gleam/list +import gleam/option.{type Option} +import gleam/otp/actor +import gleam/pgo +import gleam/result + +pub type State { + State(db: pgo.Connection, search: TypeSearch) +} + +pub type Msg { + Find(Subject(Option(List(Int))), String) + Add(String, Int) +} + +pub fn init(db: pgo.Connection) { + let init = fn() { + let search = + compute_rows(0, db, #(0, type_search.empty()), { + fn(search: #(Int, TypeSearch), row: #(String, Int)) { + let #(signature, id) = row + signature + |> parse.parse_function + |> result.map(fn(kind) { + #(search.0 + 1, type_search.add(search.1, kind, id)) + }) + |> result.unwrap(search) + } + }) + process.new_selector() + |> process.selecting(process.new_subject(), function.identity) + |> actor.Ready(State(db, search.1), _) + } + actor.start_spec(actor.Spec(init, init_timeout: 10_000, loop: loop)) +} + +fn loop(msg: Msg, state: State) -> actor.Next(Msg, State) { + case msg { + Find(subject, signature) -> { + signature + |> parse.parse_function + |> result.map(type_search.find(state.search, _)) + |> result.unwrap(option.None) + |> function.tap(fn(res) { process.send(subject, res) }) + actor.continue(state) + } + Add(signature, id) -> { + signature + |> parse.parse_function + |> result.map(fn(kind) { type_search.add(state.search, kind, id) }) + |> result.unwrap(state.search) + |> fn(s) { State(..state, search: s) } + |> actor.continue + } + } + actor.continue(state) +} + +fn compute_rows( + offset: Int, + db: pgo.Connection, + default: a, + next: fn(a, #(String, Int)) -> a, +) { + let decoder = dynamic.tuple2(dynamic.string, dynamic.int) + let rows = + "SELECT signature_, id + FROM package_type_fun_signature + WHERE kind = 'function' + ORDER BY id ASC + LIMIT 1000 + OFFSET $1" + |> pgo.execute(db, [pgo.int(offset)], decoder) + |> result.map(fn(r) { r.rows }) + |> result.unwrap([]) + use <- bool.guard(when: list.is_empty(rows), return: default) + list.fold(rows, default, next) + |> compute_rows(offset + 1000, db, _, next) +} diff --git a/apps/backend/src/backend/postgres/postgres.gleam b/apps/backend/src/backend/postgres/postgres.gleam index d8baafd..6ca2eb4 100644 --- a/apps/backend/src/backend/postgres/postgres.gleam +++ b/apps/backend/src/backend/postgres/postgres.gleam @@ -19,6 +19,7 @@ pub fn connect(cnf: Config) { hex_api_key: cnf.hex_api_key, github_token: cnf.github_token, env: cnf.env, + type_search_subject: option.None, ) } } diff --git a/apps/backend/src/backend/postgres/queries.gleam b/apps/backend/src/backend/postgres/queries.gleam index ec4cc66..e22485c 100644 --- a/apps/backend/src/backend/postgres/queries.gleam +++ b/apps/backend/src/backend/postgres/queries.gleam @@ -7,6 +7,7 @@ import gleam/bool import gleam/dict.{type Dict} import gleam/dynamic import gleam/hexpm +import gleam/int import gleam/json import gleam/list import gleam/option.{type Option, None, Some} @@ -385,7 +386,8 @@ pub fn upsert_package_type_fun_signature( parameters = $6, metadata = $7, deprecation = $9, - implementations = $10" + implementations = $10 + RETURNING id" |> pgo.execute( db, [ @@ -412,10 +414,10 @@ pub fn upsert_package_type_fun_signature( |> option.map(pgo.text) |> option.unwrap(pgo.null()), ], - dynamic.dynamic, + dynamic.element(0, dynamic.int), ) |> result.map_error(error.DatabaseError) - |> result.replace(Nil) + |> result.map(fn(r) { r.rows }) } pub fn name_search(db: pgo.Connection, query: String) { @@ -604,6 +606,35 @@ pub fn module_search(db: pgo.Connection, q: String) { |> result.map(fn(r) { r.rows }) } +pub fn exact_type_search(db: pgo.Connection, q: List(Int)) { + let ids = + list.index_map(q, fn(_, idx) { "$" <> int.to_string(idx + 1) }) + |> string.join(", ") + { "SELECT DISTINCT ON (package_rank, type_name, signature_kind, module_name) + s.name type_name, + s.documentation, + s.kind signature_kind, + s.metadata, + s.json_signature, + m.name module_name, + p.name, + r.version, + p.rank package_rank, + string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering + FROM package_type_fun_signature s + JOIN package_module m + ON m.id = s.package_module_id + JOIN package_release r + ON m.package_release_id = r.id + JOIN package p + ON p.id = r.package_id + WHERE s.id IN (" <> ids <> ") + ORDER BY package_rank DESC, type_name, signature_kind, module_name, ordering DESC" } + |> pgo.execute(db, list.map(q, pgo.int), decode_type_search) + |> result.map_error(error.DatabaseError) + |> result.map(fn(r) { r.rows }) +} + pub fn select_gleam_toml(db: pgo.Connection, offset: Int) { "SELECT gleam_toml FROM package_release diff --git a/apps/backend/src/backend/router.gleam b/apps/backend/src/backend/router.gleam index fe27803..badcdf7 100644 --- a/apps/backend/src/backend/router.gleam +++ b/apps/backend/src/backend/router.gleam @@ -1,14 +1,17 @@ import api/hex import backend/config.{type Context} import backend/error +import backend/gleam/type_search/state as type_search import backend/postgres/queries import backend/web import cors_builder as cors import gleam/bool +import gleam/erlang/process import gleam/http import gleam/int import gleam/json import gleam/list +import gleam/option import gleam/pair import gleam/result import gleam/string @@ -48,21 +51,38 @@ fn isolate_filters(query: String) -> #(String, List(String)) { fn search(query: String, ctx: Context) { wisp.log_notice("Searching for " <> query) let #(query, filters) = isolate_filters(query) + + let exact_type_searches = + option.then(ctx.type_search_subject, fn(subject) { + process.try_call(subject, type_search.Find(_, query), within: 10_000) + |> option.from_result + |> option.flatten + }) + |> option.unwrap([]) + |> queries.exact_type_search(ctx.db, _) + |> result.map_error(error.debug_log) + |> result.unwrap([]) + let exact_matches = case list.contains(filters, "in:name") { False -> [] True -> queries.name_search(ctx.db, query) |> result.map_error(error.debug_log) |> result.unwrap([]) + |> list.filter(fn(i) { !list.contains(exact_type_searches, i) }) } + let matches = case list.contains(filters, "in:signature") { False -> [] True -> queries.content_search(ctx.db, query) |> result.map_error(error.debug_log) |> result.unwrap([]) - |> list.filter(fn(i) { !list.contains(exact_matches, i) }) + |> list.filter(fn(i) { + !list.contains(list.concat([exact_matches, exact_type_searches]), i) + }) } + let signature_searches = case list.contains(filters, "in:signature") { False -> [] True -> @@ -70,9 +90,13 @@ fn search(query: String, ctx: Context) { |> result.map_error(error.debug_log) |> result.unwrap([]) |> list.filter(fn(i) { - !list.contains(list.append(exact_matches, matches), i) + !list.contains( + list.concat([exact_matches, exact_type_searches, matches]), + i, + ) }) } + let documentation_searches = case list.contains(filters, "in:documentation") { False -> [] True -> @@ -80,9 +104,18 @@ fn search(query: String, ctx: Context) { |> result.map_error(error.debug_log) |> result.unwrap([]) |> list.filter(fn(i) { - !list.contains(list.append(exact_matches, matches), i) + !list.contains( + list.concat([ + exact_matches, + exact_type_searches, + matches, + signature_searches, + ]), + i, + ) }) } + let module_searches = case list.contains(filters, "in:module") { False -> [] True -> @@ -90,10 +123,24 @@ fn search(query: String, ctx: Context) { |> result.map_error(error.debug_log) |> result.unwrap([]) |> list.filter(fn(i) { - !list.contains(list.append(exact_matches, matches), i) + !list.contains( + list.concat([ + exact_matches, + exact_type_searches, + matches, + signature_searches, + documentation_searches, + ]), + i, + ) }) } + json.object([ + #( + "exact-type-matches", + json.array(exact_type_searches, queries.type_search_to_json), + ), #("exact-matches", json.array(exact_matches, queries.type_search_to_json)), #("matches", json.array(matches, queries.type_search_to_json)), #("searches", json.array(signature_searches, queries.type_search_to_json)), diff --git a/apps/backend/src/tasks/hex.gleam b/apps/backend/src/tasks/hex.gleam index 23176ef..f633b55 100644 --- a/apps/backend/src/tasks/hex.gleam +++ b/apps/backend/src/tasks/hex.gleam @@ -5,9 +5,11 @@ import backend/config.{type Context} import backend/data/hex_read.{type HexRead} import backend/error.{type Error} import backend/gleam/context +import backend/gleam/type_search/state as type_search import backend/postgres/queries import birl.{type Time} import birl/duration +import gleam/erlang/process.{type Subject} import gleam/function import gleam/hexpm.{type Package} import gleam/int @@ -29,6 +31,7 @@ type State { hex_api_key: String, last_logged: Time, db: pgo.Connection, + type_search_subject: Option(Subject(type_search.Msg)), ) } @@ -46,6 +49,7 @@ pub fn sync_new_gleam_releases( hex_api_key: ctx.hex_api_key, last_logged: birl.now(), db: ctx.db, + type_search_subject: ctx.type_search_subject, ), children, )) @@ -122,6 +126,7 @@ pub fn sync_package(ctx: Context, package: hexpm.Package) { hex_api_key: ctx.hex_api_key, last_logged: birl.now(), db: ctx.db, + type_search_subject: ctx.type_search_subject, ), package, ) @@ -272,7 +277,13 @@ fn do_extract_package( use #(package, gleam_toml) <- result.try({ extract_release_interfaces(state, id, package, release, interfaces) }) - context.Context(state.db, package, gleam_toml, ignore_errors) + context.Context( + state.db, + package, + gleam_toml, + ignore_errors, + state.type_search_subject, + ) |> signatures.extract_signatures() |> result.map(fn(content) { let release = package.name <> " v" <> release.version diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 8b69b19..78fd5fe 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -74,8 +74,8 @@ pub fn update_search_results( let index = compute_index(search_results) let view_cache = case search_results { search_result.Start | search_result.InternalServerError -> model.view_cache - search_result.SearchResults(e, m, s, d, mods) -> - cache.cache_search_results(index, e, m, s, d, mods) + search_result.SearchResults(types, e, m, s, d, mods) -> + cache.cache_search_results(index, types, e, m, s, d, mods) |> dict.insert(model.view_cache, key, _) } Model( @@ -102,8 +102,9 @@ pub fn reset(model: Model) { fn compute_index(search_results: SearchResults) -> Index { case search_results { search_result.Start | search_result.InternalServerError -> [] - search_result.SearchResults(exact, others, searches, docs, modules) -> { + search_result.SearchResults(types, exact, others, searches, docs, modules) -> { [] + |> insert_module_names(types) |> insert_module_names(exact) |> insert_module_names(others) |> insert_module_names(searches) diff --git a/apps/frontend/src/data/search_result.gleam b/apps/frontend/src/data/search_result.gleam index e196e9a..b8b32e6 100644 --- a/apps/frontend/src/data/search_result.gleam +++ b/apps/frontend/src/data/search_result.gleam @@ -21,6 +21,7 @@ pub type SearchResults { Start InternalServerError SearchResults( + exact_type_matches: List(SearchResult), exact_matches: List(SearchResult), matches: List(SearchResult), signature_searches: List(SearchResult), @@ -48,8 +49,9 @@ pub fn decode_search_results(dyn) { dynamic.decode1(fn(_) { InternalServerError }, { dynamic.field("error", dynamic.string) }), - dynamic.decode5( + dynamic.decode6( SearchResults, + dynamic.field("exact-type-matches", dynamic.list(decode_search_result)), dynamic.field("exact-matches", dynamic.list(decode_search_result)), dynamic.field("matches", dynamic.list(decode_search_result)), dynamic.field("searches", dynamic.list(decode_search_result)), diff --git a/apps/frontend/src/frontend/strings.gleam b/apps/frontend/src/frontend/strings.gleam index 745960b..ce0e9a8 100644 --- a/apps/frontend/src/frontend/strings.gleam +++ b/apps/frontend/src/frontend/strings.gleam @@ -1,5 +1,7 @@ pub const gloogle_description = "Gloogle can search through all public gleam packages, to help you find the function you're looking for! Enter a type or a function name to get some results." +pub const types_match = "Matched directly from the signatures of functions." + pub const exact_match = "Matched directly from the name of functions, constants or types." pub const partial_match = "Partly matched from the signature of functions, constants or types." diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index a90ab18..699e762 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -8,7 +8,6 @@ import frontend/view/body/styles as s import frontend/view/search_input/search_input import gleam/dict import gleam/int -import gleam/io import gleam/list import gleam/option.{None, Some} import gleam/result @@ -159,13 +158,13 @@ pub fn body(model: Model) { title: "Internal server error", content: frontend_strings.internal_server_error, ) - search_result.SearchResults([], [], [], [], []) -> + search_result.SearchResults([], [], [], [], [], []) -> empty_state( image: images.shadow_lucy, title: "No match found!", content: frontend_strings.retry_query, ) - search_result.SearchResults(_, _, _, _, _) -> { + search_result.SearchResults(_, _, _, _, _, _) -> { dict.get(model.view_cache, model.submitted_input) |> result.map(fn(content) { el.element( diff --git a/apps/frontend/src/frontend/view/body/cache.gleam b/apps/frontend/src/frontend/view/body/cache.gleam index 678f0bc..e1ab22c 100644 --- a/apps/frontend/src/frontend/view/body/cache.gleam +++ b/apps/frontend/src/frontend/view/body/cache.gleam @@ -10,7 +10,6 @@ import frontend/view/documentation import frontend/view/types as t import gleam/bool import gleam/dict -import gleam/dynamic import gleam/io import gleam/list import gleam/option @@ -113,6 +112,7 @@ fn sidebar(index: List(#(#(String, String), List(#(String, String))))) { pub fn cache_search_results( index: List(#(#(String, String), List(#(String, String)))), + types: List(search_result.SearchResult), exact: List(search_result.SearchResult), others: List(search_result.SearchResult), searches: List(search_result.SearchResult), @@ -122,6 +122,8 @@ pub fn cache_search_results( s.search_results_wrapper([], [ sidebar(index), s.items_wrapper([], [ + match_title(types, "Types matches", frontend_strings.types_match), + view_search_results(types), match_title(exact, "Exact matches", frontend_strings.exact_match), view_search_results(exact), match_title(others, "Signature matches", frontend_strings.partial_match), diff --git a/packages/lustre/manifest.toml b/packages/lustre/manifest.toml index 0dd508b..cc9d928 100644 --- a/packages/lustre/manifest.toml +++ b/packages/lustre/manifest.toml @@ -25,7 +25,7 @@ packages = [ [requirements] birdie = { version = "~> 1.0" } gleam_erlang = { version = "~> 0.24" } -gleam_json = { version = "~> 1.0" } +gleam_json = { version = "~> 1.0 or ~> 2.0" } gleam_otp = { version = "~> 0.9" } gleam_stdlib = { version = "~> 0.36" } gleeunit = { version = "~> 1.0" }