Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions lib/spectral/abstract_code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
35 changes: 35 additions & 0 deletions test/spectral_nonempty_list_test.exs
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/support/all_types_module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions test/support/nonempty_list_module.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule NonemptyListModule do
@moduledoc false
use Spectral

@type shorthand :: %{tags: [String.t(), ...]}
@type longhand :: %{tags: nonempty_list(String.t())}
end
2 changes: 2 additions & 0 deletions test/type_info_equivalence_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading