From 33145f6fa2a9164ddf1e120ccccf6723cae97122 Mon Sep 17 00:00:00 2001 From: Andreas Hasselberg Date: Mon, 14 Sep 2026 08:28:24 +0200 Subject: [PATCH 1/2] Support [elem, ...] and [...] non-empty list shorthands in AbstractCode Co-Authored-By: Claude Opus 5 --- lib/spectral/abstract_code.ex | 10 ++++++++ test/spectral_nonempty_list_test.exs | 35 ++++++++++++++++++++++++++++ test/support/all_types_module.ex | 2 ++ test/support/nonempty_list_module.ex | 7 ++++++ test/type_info_equivalence_test.exs | 2 ++ 5 files changed, 56 insertions(+) create mode 100644 test/spectral_nonempty_list_test.exs create mode 100644 test/support/nonempty_list_module.ex diff --git a/lib/spectral/abstract_code.ex b/lib/spectral/abstract_code.ex index 46b02d3..ab69bd7 100644 --- a/lib/spectral/abstract_code.ex +++ b/lib/spectral/abstract_code.ex @@ -256,6 +256,16 @@ defmodule Spectral.AbstractCode do sp_function(args: args, return: return) end + # Shorthand non-empty list type [...] — means nonempty_list() + defp convert_type([{:..., _meta, _}]) do + sp_nonempty_list(type: sp_simple_type(type: :term)) + end + + # Shorthand non-empty list type [elem_type, ...] — means nonempty_list(elem_type) + defp convert_type([elem_ast, {:..., _meta, _}]) do + sp_nonempty_list(type: convert_type(elem_ast)) + end + # Shorthand list type [elem_type] — means list(elem_type), i.e. possibly empty defp convert_type([elem_ast]) do sp_list(type: convert_type(elem_ast)) diff --git a/test/spectral_nonempty_list_test.exs b/test/spectral_nonempty_list_test.exs new file mode 100644 index 0000000..175eb5e --- /dev/null +++ b/test/spectral_nonempty_list_test.exs @@ -0,0 +1,35 @@ +defmodule SpectralNonemptyListTest do + use ExUnit.Case, async: true + + describe "[elem, ...] shorthand" do + test "encode/decode round-trips a non-empty list" do + data = %{tags: ["a", "b"]} + + assert {:ok, encoded} = + Spectral.encode(data, NonemptyListModule, :shorthand, :json, [:pre_encoded]) + + assert {:ok, ^data} = + Spectral.decode(encoded, NonemptyListModule, :shorthand, :json, [:pre_decoded]) + end + + test "encode rejects an empty list" do + assert {:error, [%Spectral.Error{}]} = + Spectral.encode(%{tags: []}, NonemptyListModule, :shorthand, :json, [:pre_encoded]) + end + + test "decode rejects an empty list" do + assert {:error, [%Spectral.Error{}]} = + Spectral.decode(%{"tags" => []}, NonemptyListModule, :shorthand, :json, [ + :pre_decoded + ]) + end + + test "schema has minItems 1, matching nonempty_list(elem)" do + shorthand = Spectral.schema(NonemptyListModule, :shorthand, :json_schema, [:pre_encoded]) + longhand = Spectral.schema(NonemptyListModule, :longhand, :json_schema, [:pre_encoded]) + + assert shorthand[:properties]["tags"][:minItems] == 1 + assert shorthand == longhand + end + end +end diff --git a/test/support/all_types_module.ex b/test/support/all_types_module.ex index f8ae4bc..7b3e31e 100644 --- a/test/support/all_types_module.ex +++ b/test/support/all_types_module.ex @@ -56,6 +56,8 @@ defmodule AllTypesModule do @type t_list_shorthand :: [atom()] @type t_nonempty_list :: nonempty_list(integer()) @type t_nonempty_list_any :: nonempty_list() + @type t_nonempty_list_shorthand :: [integer(), ...] + @type t_nonempty_list_shorthand_any :: [...] # Tuple types @type t_tuple_any :: tuple() diff --git a/test/support/nonempty_list_module.ex b/test/support/nonempty_list_module.ex new file mode 100644 index 0000000..3e16448 --- /dev/null +++ b/test/support/nonempty_list_module.ex @@ -0,0 +1,7 @@ +defmodule NonemptyListModule do + @moduledoc false + use Spectral + + @type shorthand :: %{tags: [String.t(), ...]} + @type longhand :: %{tags: nonempty_list(String.t())} +end diff --git a/test/type_info_equivalence_test.exs b/test/type_info_equivalence_test.exs index c7a276f..808e39d 100644 --- a/test/type_info_equivalence_test.exs +++ b/test/type_info_equivalence_test.exs @@ -206,6 +206,8 @@ defmodule TypeInfoEquivalenceTest do :t_list_typed, :t_list_shorthand, :t_nonempty_list, + :t_nonempty_list_shorthand, + :t_nonempty_list_shorthand_any, :t_tuple_any, :t_tuple2, :t_tuple3, From e1aae80b1b56525b1985859a332de32f67ba5f8d Mon Sep 17 00:00:00 2001 From: Andreas Hasselberg Date: Mon, 14 Sep 2026 10:34:14 +0200 Subject: [PATCH 2/2] Add changelog entry for non-empty list shorthand fix Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f886566..de3a9b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- `Spectral.AbstractCode` now handles the Elixir non-empty list shorthand, `[elem_type, ...]` (and bare `[...]`), matching the existing `nonempty_list(elem_type)` support. Previously these types failed to compile with `unsupported type AST`. + ## [0.13.0] - 2026-05-07 ### Added