@@ -580,6 +580,222 @@ defmodule SchemectoTest do
580580 }
581581 }
582582 end
583+ end
584+
585+ describe "to_json_schema + validations" do
586+ test "extracts format validation to pattern" do
587+ types = % { email: :string }
588+
589+ changeset =
590+ Schemecto . new ( types )
591+ |> Ecto.Changeset . validate_format ( :email , ~r/ @/ )
592+
593+ result = Schemecto . to_json_schema ( changeset )
594+
595+ assert result == % {
596+ "type" => "object" ,
597+ "properties" => % {
598+ "email" => % { "type" => "string" , "pattern" => "@" }
599+ }
600+ }
601+ end
602+
603+ test "extracts inclusion validation to enum for lists" do
604+ types = % { status: :string }
605+
606+ changeset =
607+ Schemecto . new ( types )
608+ |> Ecto.Changeset . validate_inclusion ( :status , [ "pending" , "active" , "completed" ] )
609+
610+ result = Schemecto . to_json_schema ( changeset )
611+
612+ assert result == % {
613+ "type" => "object" ,
614+ "properties" => % {
615+ "status" => % { "type" => "string" , "enum" => [ "pending" , "active" , "completed" ] }
616+ }
617+ }
618+ end
619+
620+ test "extracts inclusion validation with range to min/max" do
621+ types = % { age: :integer }
622+
623+ changeset =
624+ Schemecto . new ( types )
625+ |> Ecto.Changeset . validate_inclusion ( :age , 0 .. 120 )
626+
627+ result = Schemecto . to_json_schema ( changeset )
628+
629+ assert result == % {
630+ "type" => "object" ,
631+ "properties" => % {
632+ "age" => % { "type" => "integer" , "minimum" => 0 , "maximum" => 120 }
633+ }
634+ }
635+ end
636+
637+ test "extracts length validation for strings with min/max" do
638+ types = % { title: :string }
639+
640+ changeset =
641+ Schemecto . new ( types )
642+ |> Ecto.Changeset . validate_length ( :title , min: 1 , max: 100 )
643+
644+ result = Schemecto . to_json_schema ( changeset )
645+
646+ assert result == % {
647+ "type" => "object" ,
648+ "properties" => % {
649+ "title" => % { "type" => "string" , "minLength" => 1 , "maxLength" => 100 }
650+ }
651+ }
652+ end
653+
654+ test "extracts length validation for strings with is" do
655+ types = % { code: :string }
656+
657+ changeset =
658+ Schemecto . new ( types )
659+ |> Ecto.Changeset . validate_length ( :code , is: 6 )
660+
661+ result = Schemecto . to_json_schema ( changeset )
662+
663+ assert result == % {
664+ "type" => "object" ,
665+ "properties" => % {
666+ "code" => % { "type" => "string" , "minLength" => 6 , "maxLength" => 6 }
667+ }
668+ }
669+ end
670+
671+ test "extracts length validation for arrays" do
672+ types = % { tags: { :array , :string } }
673+
674+ changeset =
675+ Schemecto . new ( types )
676+ |> Ecto.Changeset . validate_length ( :tags , min: 1 , max: 5 )
677+
678+ result = Schemecto . to_json_schema ( changeset )
679+
680+ assert result == % {
681+ "type" => "object" ,
682+ "properties" => % {
683+ "tags" => % {
684+ "type" => "array" ,
685+ "items" => % { "type" => "string" } ,
686+ "minItems" => 1 ,
687+ "maxItems" => 5
688+ }
689+ }
690+ }
691+ end
692+
693+ test "extracts number validations" do
694+ types = % { age: :integer }
695+
696+ changeset =
697+ Schemecto . new ( types )
698+ |> Ecto.Changeset . validate_number ( :age , greater_than_or_equal_to: 0 , less_than: 150 )
699+
700+ result = Schemecto . to_json_schema ( changeset )
701+
702+ assert result == % {
703+ "type" => "object" ,
704+ "properties" => % {
705+ "age" => % { "type" => "integer" , "minimum" => 0 , "exclusiveMaximum" => 150 }
706+ }
707+ }
708+ end
709+
710+ test "extracts subset validation for array items" do
711+ types = % { tags: { :array , :string } }
712+
713+ changeset =
714+ Schemecto . new ( types )
715+ |> Ecto.Changeset . validate_subset ( :tags , [ "elixir" , "erlang" , "ecto" ] )
716+
717+ result = Schemecto . to_json_schema ( changeset )
718+
719+ assert result == % {
720+ "type" => "object" ,
721+ "properties" => % {
722+ "tags" => % {
723+ "type" => "array" ,
724+ "items" => % { "type" => "string" , "enum" => [ "elixir" , "erlang" , "ecto" ] }
725+ }
726+ }
727+ }
728+ end
729+
730+ test "handles multiple validations on same field" do
731+ types = % { title: :string }
732+
733+ changeset =
734+ Schemecto . new ( types )
735+ |> Ecto.Changeset . validate_length ( :title , min: 1 )
736+ |> Ecto.Changeset . validate_length ( :title , max: 100 )
737+ |> Ecto.Changeset . validate_format ( :title , ~r/ ^[A-Z]/ )
738+
739+ result = Schemecto . to_json_schema ( changeset )
740+
741+ assert result == % {
742+ "type" => "object" ,
743+ "properties" => % {
744+ "title" => % {
745+ "type" => "string" ,
746+ "minLength" => 1 ,
747+ "maxLength" => 100 ,
748+ "pattern" => "^[A-Z]"
749+ }
750+ }
751+ }
752+ end
753+
754+ test "handles nested validations in one/many" do
755+ address_types = % { street: :string , zip: :string }
756+
757+ validate_address = fn changeset , params ->
758+ changeset
759+ |> Ecto.Changeset . cast ( params , [ :street , :zip ] )
760+ |> Ecto.Changeset . validate_required ( [ :street ] )
761+ |> Ecto.Changeset . validate_length ( :zip , is: 5 )
762+ end
763+
764+ types = % {
765+ name: :string ,
766+ address: Schemecto . one ( address_types , with: validate_address )
767+ }
768+
769+ changeset = Schemecto . new ( types )
770+ result = Schemecto . to_json_schema ( changeset )
771+
772+ assert result == % {
773+ "type" => "object" ,
774+ "properties" => % {
775+ "name" => % { "type" => "string" } ,
776+ "address" => % {
777+ "type" => "object" ,
778+ "properties" => % {
779+ "street" => % { "type" => "string" } ,
780+ "zip" => % { "type" => "string" , "minLength" => 5 , "maxLength" => 5 }
781+ } ,
782+ "required" => [ "street" ]
783+ }
784+ }
785+ }
786+ end
787+
788+ test "raises when format validation on non-string field" do
789+ types = % { age: :integer }
790+
791+ changeset =
792+ Schemecto . new ( types )
793+ |> Ecto.Changeset . validate_format ( :age , ~r/ \d +/ )
794+
795+ assert_raise ArgumentError , "validate_format can only be applied to string fields" , fn ->
796+ Schemecto . to_json_schema ( changeset )
797+ end
798+ end
583799
584800 test "raises error for unknown type" do
585801 types = % { name: :unknown_type }
0 commit comments