### Spark DSL Extension Setup Source: https://github.com/ash-project/spark/blob/main/README.md Uses Spark.Dsl.Extension to set up a DSL extension, incorporating defined sections like 'fields'. This is a core step in building a Spark-based DSL. ```elixir use Spark.Dsl.Extension, sections: [@fields] ``` -------------------------------- ### Elixir Spark DSL Error Handling Example Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Shows the structure for creating a Spark DslError with essential context like message, path, module, and location. ```elixir {:error, Spark.Error.DslError.exception( message: "Clear error message", path: [:section, :subsection], module: module, location: annotation )} ``` -------------------------------- ### Elixir Info Module Definition and Usage Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Demonstrates how to define a custom Info Module for DSL introspection and provides examples of its usage for accessing entities and options. ```elixir # Define an info module for your extension defmodule MyLibrary.Info do use Spark.InfoGenerator, extension: MyLibrary.Dsl, sections: [:my_section] end # The info generator creates accessor functions for DSL data: # - MyLibrary.Info.my_section_entities(module) # - MyLibrary.Info.my_section_option!(module, :option_name) # - MyLibrary.Info.my_section_option(module, :option_name, default) # Example usage: entities = MyLibrary.Info.my_section_entities(MyApp.Example) required_option = MyLibrary.Info.my_section_option!(MyApp.Example, :required) {:ok,optional_value} = MyLibrary.Info.my_section_option(MyApp.Example, :optional, "default") ``` -------------------------------- ### Spark DSL Creation and Usage Example Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Demonstrates the step-by-step process of creating a Spark DSL, including defining entities, sections, and extensions, and then using the DSL in an application module. Shows nested entity definitions and usage. ```elixir # Step 1: Define your extension defmodule MyLibrary.Dsl do @my_nested_entity %Spark.Dsl.Entity{ name: :my_nested_entity, target: MyLibrary.MyNestedEntity, describe: """ Describe the entity. """, examples: [ "my_nested_entity :name, option: \"something\"", """ my_nested_entity :name do option \"something\" end """ ], args: [:name], schema: [ name: [type: :atom, required: true, doc: "The name of the entity"], option: [type: :string, default: "default", doc: "An option that does X"] ] } @my_entity %Spark.Dsl.Entity{ name: :my_entity, target: MyLibrary.MyEntity, args: [:name], entities: [ entities: [@my_nested_entity] ], describe: ..., # omitted for brevity examples: ..., # omitted for brevity schema: [ name: [type: :atom, required: true, doc: "The name of the entity"], option: [type: :string, default: "default", doc: "An option that does X"] ] } @my_section %Spark.Dsl.Section{ name: :my_section, describe: ..., # omitted for brevity examples: ..., # omitted for brevity schema: [ section_option: [type: :string] ], entities: [@my_entity] } use Spark.Dsl.Extension, sections: [@my_section] end # Entity Targets defmodule MyLibrary.MyEntity do defstruct [:name, :option, :entities, :__spark_metadata__] end defmodule MyLibrary.MyNestedEntity do defstruct [:name, :option, :__spark_metadata__] end # Step 2: Create the DSL module defmodule MyLibrary do use Spark.Dsl, default_extensions: [ extensions: [MyLibrary.Dsl] ] end # Step 3: Use the DSL defmodule MyApp.Example do use MyLibrary my_section do my_entity :example_name do option "custom value" my_nested_entity :nested_name do option "nested custom value" end end end end ``` -------------------------------- ### Enable Autocomplete for Function Options with Spark.Options Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/setup-autocomplete.md Add `@doc` metadata with argument index and schema to enable autocomplete and documentation for function options. This example shows how to define and validate options for the `do_something` function. ```elixir @schema [ verbose?: [ type: :boolean, doc: "Whether or not to log verbose messages to the console", default: false ] ] @doc spark_opts: [{1, @schema}] def do_something(arg, opts \ []) do opts = Spark.Options.validate!(opts, @schema) ... end ``` -------------------------------- ### Quick Example: Data Validator DSL Source: https://github.com/ash-project/spark/blob/main/README.md Defines a data validator DSL for a 'Person' struct, including required fields, type checking, and value transformation. This snippet also shows how to use the validator. ```elixir defmodule MyApp.PersonValidator do use MyLibrary.Validator fields do required [:name] field :name, :string field :email, :string do check &String.contains?(&1, "@") transform &String.trim/1 end end end MyApp.PersonValidator.validate(%{name: "Zach", email: " foo@example.com "}) {:ok, %{name: "Zach", email: "foo@example.com"}} ``` -------------------------------- ### Essential Spark DSL API Functions Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Provides examples of essential Spark DSL API functions for retrieving entities and options, adding entities during transformation, and persisting data across transformers. ```elixir # Get entities from a section entities = Spark.Dsl.Extension.get_entities(dsl_state, [:section_path]) # Get section options option = Spark.Dsl.Extension.get_opt(dsl_state, [:section_path], :option_name, default_value) # Add entities during transformation dsl_state = Spark.Dsl.Transformer.add_entity(dsl_state, [:section_path], entity) # Persist data across transformers dsl_state = Spark.Dsl.Transformer.persist(dsl_state, :key, value) value = Spark.Dsl.Transformer.get_persisted(dsl_state, :key) # Get current module being compiled module = Spark.Dsl.Verifier.get_persisted(dsl_state, :module) # Get annotation information for error reporting section_anno = Spark.Dsl.Transformer.get_section_anno(dsl_state, [:section_path]) option_anno = Spark.Dsl.Transformer.get_opt_anno(dsl_state, [:section_path], :option_name) entity_anno = Spark.Dsl.Entity.anno(entity) ``` -------------------------------- ### Define a Simple Validator DSL Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This example shows how to define a basic data validator DSL using Spark. It includes required fields, type checking, and value transformation. The commented-out line demonstrates an alternative syntax for defining fields. ```elixir defmodule MyApp.PersonValidator do use MyLibrary.Validator fields do required [:name] field :name, :string field :email, :string do check &String.contains?(&1, "@") transform &String.trim/1 end # This syntax is also supported # field :email, :string, check: &String.contains?(&1, "@"), transform: &String.trim/1 end end MyApp.PersonValidator.validate(%{name: "Zach", email: " foo@example.com "}) {:ok, %{name: "Zach", email: "foo@example.com"}} MyApp.PersonValidator.validate(%{name: "Zach", email: " blank "}) :error ``` -------------------------------- ### Example of an Invalid Validator Definition Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This snippet shows an example of a Spark module that would fail validation due to an invalid configuration. It defines required fields that are not present in the fields list. ```elixir defmodule MyApp.BadValidator do use MyLibrary.Validator fields do required [:name, :email] field :name, :string end end ``` -------------------------------- ### Elixir Spark Error Location Information Examples Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Demonstrates how to retrieve and use location information (entity, section, or option annotations) when constructing Spark DslErrors for better debugging. ```elixir # For entity-related errors, get entity annotations entity_location = Spark.Dsl.Entity.anno(entity) # For section-related errors, get section annotations section_location = Spark.Dsl.Transformer.get_section_anno(dsl_state, [:section_path]) # For option-related errors, get option annotations option_location = Spark.Dsl.Transformer.get_opt_anno(dsl_state, [:section_path], :option_name) # Include in DslError {:error, Spark.Error.DslError.exception( message: "Detailed error message", path: [:section_path], module: module, location: entity_location # or section_location, option_location )} ``` -------------------------------- ### Get File Path from Annotation Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Retrieves the file path associated with an annotation. Returns :undefined if no file is associated. ```elixir # Get the file (returns :undefined or a charlist) file = :erl_anno.file(anno) ``` -------------------------------- ### Verify Transformer Effect on DSL Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md After applying a transformer, you can re-inspect the DSL using functions like `MyLibrary.Validator.Info.fields/1` to confirm the changes. This example shows the `:id` field being added. ```elixir iex(1)> MyLibrary.Validator.Info.fields(MyApp.PersonValidator) [ %MyLibrary.Validator.Dsl.Field{ name: :id, type: :string, transform: nil, check: nil }, %MyLibrary.Validator.Dsl.Field{ name: :name, type: :string, transform: nil, check: nil }, %MyLibrary.Validator.Dsl.Field{ name: :email, type: :string, transform: &String.trim/1, check: &MyApp.PersonValidator.check_0_generated_18E6D5D8C34DFA0EDA8E926DAAEE7E52/1 } ] ``` -------------------------------- ### Get DSL Entities Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Use `Spark.Dsl.Extension.get_entities/2` to retrieve specific entities from your DSL. This is useful for inspecting the structure and details of your defined DSL elements. ```elixir iex(1)> Spark.Dsl.Extension.get_entities(MyApp.PersonValidator, :fields) iex(2)> Spark.Dsl.Extension.get_entities(MyApp.PersonValidator, :fields) [ %MyLibrary.Validator.Dsl.Field{ name: :name, type: :string, transform: nil, check: nil }, %MyLibrary.Validator.Dsl.Field{ name: :email, type: :string, transform: &String.trim/1, # This is an example of some under the hood magic that spark does # to allow you to define a function inside your DSL. This sort of thing # is quite difficult to do with hand-rolled DSLs. check: &MyApp.PersonValidator.check_0_generated_18E6D5D8C34DFA0EDA8E926DAAEE7E52/1 } ] ``` -------------------------------- ### Get End Location from Annotation Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Retrieves the end location of an annotation, available in OTP 28 and later. Returns :undefined if not available. ```elixir # Get the end location (OTP 28+, returns :undefined if not available) if function_exported?(:erl_anno, :end_location, 1) do end_location = :erl_anno.end_location(anno) end ``` -------------------------------- ### Spark Transformer for Adding Default Entities Source: https://github.com/ash-project/spark/blob/main/usage-rules.md An example of a Spark transformer that adds a default entity to a section if no entities are present. It demonstrates how to access and modify DSL state during the compile-time processing pipeline. ```elixir defmodule MyLibrary.Transformers.AddDefaults do use Spark.Dsl.Transformer def transform(dsl_state) do # Add a default entity if none exist entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section]) if Enum.empty?(entities) do {:ok, Spark.Dsl.Transformer.add_entity( dsl_state, [:my_section], %MyLibrary.MyEntity{name: :default} )} else {:ok, dsl_state} end end # Control execution order def after?(_), do: false def before?(OtherTransformer), do: true def before?(_), do: false end ``` -------------------------------- ### Error Output for Invalid Validator Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This snippet displays the expected error output when an invalid validator, as defined in the previous example, is processed. It highlights the DSL error message and the stack trace. ```elixir ** (Spark.Error.DslError) [MyApp.BadValidator] fields -> required: All required fields must be specified in fields (elixir 1.17.2) lib/process.ex:864: Process.info/2 (spark 2.2.35) lib/spark/error/dsl_error.ex:30: Spark.Error.DslError.exception/1 iex:54: MyLibrary.Validator.Verifiers.VerifyRequired.verify/1 iex:40: anonymous fn/1 in MyApp.BadValidator.__verify_spark_dsl__/1 (elixir 1.17.2) lib/enum.ex:4353: Enum.flat_map_list/2 (elixir 1.17.2) lib/enum.ex:4354: Enum.flat_map_list/2 iex:40: MyApp.BadValidator.__verify_spark_dsl__/1 (elixir 1.17.2) lib/enum.ex:987: Enum.-each/2-lists^foreach/1-0-/2 ``` -------------------------------- ### Use Notification DSL in Configuration Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/build-extensions-with-builders.md Demonstrates how to consume the defined `MyApp.Notifications.Dsl` within a module's configuration. This shows the practical application of the built extension. ```elixir defmodule MyApp.Config do use MyApp.Notifications.Dsl notifications do notification :ops, :email do target "ops@example.com" metadata priority: 1 end end end ``` -------------------------------- ### IDE Integration for Finding Definitions Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Implement language server features to find DSL definitions based on file, line, and column. ```elixir defmodule MyLanguageServer do def find_definition(file, line, column) do # Find modules that might contain DSL at this location modules = find_modules_in_file(file) Enum.find_value(modules, fn module -> dsl_state = module.spark_dsl_config() # Check section annotations Enum.find_value(dsl_state, fn {path, config} -> if match_location?(config.section_anno, line) do {:section, path, config.section_anno} end end) || # Check entity annotations find_entity_at_location(dsl_state, line) end) end end ``` -------------------------------- ### Define DSL Info Interface Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Use `Spark.InfoGenerator` to automatically define functions for interacting with your DSL. Define a module like `YourDsl.Info` and use `use Spark.InfoGenerator` with the appropriate extension and sections. ```elixir defmodule MyLibrary.Validator.Info do use Spark.InfoGenerator, extension: MyLibrary.Validator.Dsl, sections: [:fields] end ``` -------------------------------- ### Debugging DSL Source Locations Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Create debugging utilities to inspect all DSL elements within a module and display their source locations. ```elixir defmodule MyLibrary.Debug do def inspect_dsl_sources(module) do dsl_state = module.spark_dsl_config() # Show all DSL elements with their locations Enum.each(dsl_state, fn {path, config} -> IO.puts("Section #{inspect(path)}:") if config.section_anno do print_location(" Section", config.section_anno) end # Show options Enum.each(config.opts_anno, fn {opt_name, anno} -> print_location(" Option #{opt_name}", anno) end) # Show entities Enum.each(config.entities, fn entity -> anno = Spark.Dsl.Entity.anno(entity) print_location(" Entity #{entity.name}", anno) end) end) end defp print_location(label, anno) defp print_location(label, nil), do: nil defp print_location(label, anno) do line = :erl_anno.location(anno) file = :erl_anno.file(anno) |> to_string() |> Path.relative_to_cwd() IO.puts(" #{label}: #{file}:#{line}") end end ``` -------------------------------- ### Erlang :erl_anno Utilities Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Demonstrates basic utilities from Erlang's :erl_anno module for checking if a value is an annotation and retrieving its location. ```elixir # Check if something is an annotation :erl_anno.is_anno(anno) # Get the location (line number or {line, column}) # Note: Spark currently only provides line numbers, not column information location = :erl_anno.location(anno) ``` -------------------------------- ### Define Notification Extension with Helper Module Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/build-extensions-with-builders.md Organizes builders into a separate module (`MyApp.Notifications.Dsl.Builder`) for cleaner DSL modules and reusable builders. This approach is recommended for more complex extensions. ```elixir defmodule MyApp.Notifications.Notification do defstruct [:name, :type, :target, :metadata, :__identifier__, :__spark_metadata__] end defmodule MyApp.Notifications.Dsl.Builder do alias Spark.Builder.{Entity, Field, Section} def notification_entity do Entity.new(:notification, MyApp.Notifications.Notification, describe: "Defines a notification delivery", args: [:name, :type], schema: [ Field.new(:name, :atom, required: true, doc: "Notification name"), Field.new(:type, {:one_of, [:email, :slack]}, required: true, doc: "Notification type" ), Field.new(:target, :string, doc: "Delivery target"), Field.new(:metadata, :keyword_list, keys: [ priority: [type: :integer, default: 0, doc: "Priority level"] ], doc: "Optional metadata" ) ], identifier: :name ) |> Entity.build!() end def notifications_section do Section.new(:notifications, describe: "Notification configuration", entities: [notification_entity()] ) |> Section.build!() end end defmodule MyApp.Notifications.Dsl do alias MyApp.Notifications.Dsl.Builder use Spark.Dsl.Extension, sections: [Builder.notifications_section()] use Spark.Dsl, default_extensions: [extensions: __MODULE__] end ``` -------------------------------- ### Avoid Deadlocks in Transformations Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Demonstrates the incorrect way to access module functions within a transformation, which can lead to deadlocks. Shows the correct approach by working directly with DSL state. ```elixir def transform(dsl_state) do module = get_persisted(dsl_state, :module) module.some_function() # Module isn't compiled yet! end ``` ```elixir def transform(dsl_state) do entities = get_entities(dsl_state, [:section]) # Work with DSL state, not module functions end ``` -------------------------------- ### Configure DSL Extension with Persisters Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Include your persister in the `persisters:` list within your `Spark.Dsl.Extension` definition. Persisters run after all transformers and are intended for caching derived values. ```elixir use Spark.Dsl.Extension, sections: [@fields], transformers: [ MyLibrary.Validator.Transformers.AddId, MyLibrary.Validator.Transformers.GenerateValidate ], persisters: [ MyLibrary.Validator.Persisters.CacheFieldNames ], verifiers: [ MyLibrary.Validator.Verifiers.VerifyRequired ] ``` -------------------------------- ### Access DSL Information with InfoGenerator Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Once defined, you can use the generated functions like `fields/1` and `fields_required/1` to query your DSL. The `!` variant (e.g., `fields_required!/1`) can be used when you expect values to always be present. ```elixir iex(1)> MyLibrary.Validator.Info.fields(MyApp.PersonValidator) [ %MyLibrary.Validator.Dsl.Field{ name: :name, type: :string, transform: nil, check: nil }, %MyLibrary.Validator.Dsl.Field{ name: :email, type: :string, transform: &String.trim/1, check: &MyApp.PersonValidator.check_0_generated_18E6D5D8C34DFA0EDA8E926DAAEE7E52/1 } ] # Returns `:error` for fields not specified iex(2)> MyLibrary.Validator.Info.fields_required(MyApp.PersonValidator) {:ok, [:name]} # The `!` version can be used for fields you know will always be set iex(3)> MyLibrary.Validator.Info.fields_required!(MyApp.PersonValidator) [:name] ``` -------------------------------- ### Elixir Spark Options Schema Definition Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Illustrates how to define a schema for DSL options using Spark.Options, specifying field types, requirements, and default values. ```elixir schema = [ required_field: [ type: :atom, required: true, doc: "Documentation for the field" ], optional_field: [ type: {:list, :string}, default: [], doc: "Optional field with default" ], complex_field: [ type: {:or, [:atom, :string, {:struct, MyStruct}]}, required: false ] ] ``` -------------------------------- ### Convert File Charlist to String Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Safely converts a file path from a charlist to a string, defaulting to 'unknown' if the file is :undefined. ```elixir # Convert charlist to string safely file_string = case file do :undefined -> "unknown" charlist -> to_string(charlist) end ``` -------------------------------- ### Configure Parser for Token Metadata Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md This configuration enables token metadata and parser columns, which are necessary for detailed source annotation tracking in Spark. ```elixir defmodule Acme.MixProject do use Mix.Project def project do [ app: :acme, elixirc_options: [ parser_options: [ token_metadata: true, parser_columns: true ] ], # ... ] end end ``` -------------------------------- ### Include Verifier in DSL Extension Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This snippet demonstrates how to include the custom verifier in a DSL extension configuration. It specifies the verifiers to be used alongside sections and transformers. ```elixir use Spark.Dsl.Extension, sections: [@fields], transformers: [ MyLibrary.Validator.Transformers.AddId ], verifiers: [ MyLibrary.Validator.Verifiers.VerifyRequired ] ``` -------------------------------- ### Introspect Section, Option, and Entity Annotations Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Access and extract source location information (file and line number) for sections, options, and entities from the DSL state. ```elixir # Get DSL state dsl_state = MyModule.spark_dsl_config() # Section annotations section_anno = Spark.Dsl.Extension.get_section_anno(dsl_state, [:my_section]) if section_anno do # Extract line number (Spark currently provides line numbers only) line = case :erl_anno.location(section_anno) do {line_num, _col} -> line_num line_num -> line_num end file = :erl_anno.file(section_anno) |> to_string() IO.puts("Section defined at #{file}:#{line}") end # Option annotations option_anno = Spark.Dsl.Extension.get_opt_anno(dsl_state, [:my_section], :option_name) if option_anno do line = :erl_anno.location(option_anno) file = :erl_anno.file(option_anno) |> to_string() IO.puts("Option defined at #{file}:#{line}") end # Entity annotations entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section]) Enum.each(entities, fn entity -> case Spark.Dsl.Entity.anno(entity) do nil -> :ok anno -> line = :erl_anno.location(anno) file = :erl_anno.file(anno) |> to_string() IO.puts("Entity defined at #{file}:#{line}") end end) ``` -------------------------------- ### Implement a Verifier Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/writing-extensions.md Define a verifier to validate DSL state after compilation. Verifiers are read-only and can return `:ok`, `{:error, term}`, or `{:warn, warning}`. They avoid circular compile-time dependencies. ```elixir defmodule MyApp.Verifiers.ValidateConfig do use Spark.Dsl.Verifier def verify(dsl_state) do name = Spark.Dsl.Verifier.get_option(dsl_state, [:my_section], :name) if name do :ok else {:error, Spark.Error.DslError.exception( message: "name is required", path: [:my_section, :name], module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module) )} end end end ``` -------------------------------- ### Spark Core Components: Entity, Section, Extension Source: https://github.com/ash-project/spark/blob/main/usage-rules.md Defines the basic building blocks of a Spark DSL: Entities for individual constructs, Sections for organizing entities, and Extensions for packaging DSL functionality. Includes schema definitions and target struct assignments. ```elixir # 1. Entities - Individual DSL constructors @field %Spark.Dsl.Entity{ name: :field, target: MyApp.Field, args: [:name, :type], schema: [ name: [type: :atom, required: true], type: [type: :atom, required: true] ] } # 2. Sections - Organize related entities @fields %Spark.Dsl.Section{ name: :fields, entities: [@field], sections: [], schema: [ required: [type: {:list, :atom}, default: []] ] } # 3. Extensions - Package DSL functionality defmodule MyExtension do use Spark.Dsl.Extension, sections: [@fields], transformers: [MyTransformer], verifiers: [MyVerifier] end ``` -------------------------------- ### Declare Extension Components Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/writing-extensions.md Declare the sections, transformers, persisters, and verifiers for your Spark extension. ```elixir use Spark.Dsl.Extension, sections: [@my_section], transformers: [MyApp.Transformers.SetDefaults], persisters: [MyApp.Persisters.CacheComputedValues], verifiers: [MyApp.Verifiers.ValidateConfig] ``` -------------------------------- ### Define the Main DSL Module Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This code defines the main DSL module that users will `use` to create their own DSLs. It configures Spark to use the previously defined validator DSL extension by default. ```elixir defmodule MyLibrary.Validator do use Spark.Dsl, default_extensions: [ extensions: [MyLibrary.Validator.Dsl] ] end ``` -------------------------------- ### Handle Missing Annotations Gracefully Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Gracefully handle cases where annotation information might be missing, such as for programmatically generated DSL elements, by providing default or alternative information. ```elixir location_info = if anno do line = :erl_anno.location(anno) file = :erl_anno.file(anno) |> to_string() " at #{file}:#{line}" else "" end IO.puts("Error in entity#{location_info}") ``` -------------------------------- ### Enabling Debug Info for Source Annotations Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md To capture source annotations (`:erl_anno.anno()`) in warning payloads, ensure `debug_info` is enabled for runtime modules compiled within tests. Add `test_elixirc_options: [debug_info: true]` to your `mix.exs`. ```elixir test_elixirc_options: [debug_info: true] ``` -------------------------------- ### Use a Spark DSL Fragment in a Resource Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/split-up-large-dsls.md Include a previously defined fragment in an Ash.Resource definition. This allows the resource to inherit the configurations and behaviors defined in the fragment. ```elixir defmodule MyApp.Accounts.User do use Ash.Resource, fragments: [MyApp.Accounts.User.Fragments.DataLayer] ... end ``` -------------------------------- ### Implement a Spark Transformer Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Transformers allow arbitrary transformations of DSL data at compile time. Implement the `Spark.Dsl.Transformer` behaviour and define the `transform/1` function to modify the `dsl_state`. ```elixir defmodule MyLibrary.Validator.Transformers.AddId do use Spark.Dsl.Transformer # dsl_state here is a map of the underlying DSL data def transform(dsl_state) do {:ok, Spark.Dsl.Transformer.add_entity(dsl_state, [:fields], %MyLibrary.Validator.Dsl.Field{ name: :id, type: :string }) } end end ``` -------------------------------- ### Enable Debug Info in ExUnit Tests Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Source annotations are not available by default in .exs files like ExUnit tests. This snippet shows how to explicitly enable debug info for tests. ```elixir setup do debug_info? = Code.get_compiler_option(:debug_info) Code.put_compiler_option(:debug_info, true) on_exit(fn -> Code.put_compiler_option(:debug_info, debug_info?) end) :ok end ``` -------------------------------- ### Asserting Literal Warning Message Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md To match a specific warning message exactly, provide the string as the first element of the pattern in `assert_dsl_warning/2`. ```elixir {_, _} = assert_dsl_warning {"legacy_option is deprecated", _} do # ... end ``` -------------------------------- ### Asserting Any DSL Error (Shorthand) Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md Use the `/1` form of `assert_dsl_error` when the specific error pattern does not matter. This helper captures the first error produced by the do-block without requiring a specific pattern match. ```elixir err = assert_dsl_error do defmodule Elixir.BadPerson do # ... end end ``` -------------------------------- ### Implement a Spark Persister Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Persisters are special transformers that run after all other transformers to cache computed values. Implement `Spark.Dsl.Transformer` and use `Spark.Dsl.Transformer.persist/3` to store cached data. ```elixir defmodule MyLibrary.Validator.Persisters.CacheFieldNames do use Spark.Dsl.Transformer def transform(dsl_state) do field_names = dsl_state |> Spark.Dsl.Transformer.get_entities([:fields]) |> Enum.map(& &1.name) {:ok, Spark.Dsl.Transformer.persist(dsl_state, :field_names, field_names)} end end ``` -------------------------------- ### Define a Custom Verifier Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This snippet shows how to define a custom verifier that checks if all required fields are present in the fields definition. It uses `Spark.Dsl.Verifier` and accesses DSL state via a map. ```elixir defmodule MyLibrary.Validator.Verifiers.VerifyRequired do use Spark.Dsl.Verifier # dsl_state here is a map of the underlying DSL data def verify(dsl_state) do # we can use our info module here, even though we are passing in a # map of data and not a module! Very handy. required = MyLibrary.Validator.Info.fields_required!(dsl_state) fields = Enum.map(MyLibrary.Validator.Info.fields(dsl_state), &(&1.name)) if Enum.all?(required, &Enum.member?(fields, &1)) do :ok else {:error, Spark.Error.DslError.exception( message: "All required fields must be specified in fields", path: [:fields, :required], # this is how you get the original module out. # only do this for display purposes. # the module is not yet compiled (we're compiling it right now!), so if you # try to call functions on it, you will deadlock the compiler # and get an error module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module) )} end end end ``` -------------------------------- ### Define Notification Extension Inline Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/build-extensions-with-builders.md Defines a 'notifications' extension with a 'notification' entity using inline builders within the `use Spark.Dsl.Extension` statement. Suitable for simpler extensions. ```elixir defmodule MyApp.Notifications.Notification do defstruct [:name, :type, :target, :metadata, :__identifier__, :__spark_metadata__] end defmodule MyApp.Notifications.Dsl do alias Spark.Builder.{Entity, Field, Section} use Spark.Dsl.Extension, sections: [ Section.new(:notifications, describe: "Notification configuration", entities: [ Entity.new(:notification, MyApp.Notifications.Notification, describe: "Defines a notification delivery", args: [:name, :type], schema: [ Field.new(:name, :atom, required: true, doc: "Notification name"), Field.new(:type, {:one_of, [:email, :slack]}, required: true, doc: "Notification type" ), Field.new(:target, :string, doc: "Delivery target"), Field.new(:metadata, :keyword_list, keys: [ priority: [type: :integer, default: 0, doc: "Priority level"] ], doc: "Optional metadata" ) ], identifier: :name ) |> Entity.build!() ] ) |> Section.build!() ] use Spark.Dsl, default_extensions: [extensions: __MODULE__] end ``` -------------------------------- ### Implement a Persister Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/writing-extensions.md Implement a persister to precompute and cache derived values after all transformers have completed. Persisters should only write to the persisted data map using `Spark.Dsl.Transformer.persist/3`. ```elixir defmodule MyApp.Persisters.CacheActionNames do use Spark.Dsl.Transformer def transform(dsl_state) do action_names = dsl_state |> Spark.Dsl.Transformer.get_entities([:actions]) |> Enum.map(& &1.name) {:ok, Spark.Dsl.Transformer.persist(dsl_state, :action_names, action_names)} end end ``` -------------------------------- ### Apply Transformer to DSL Extension Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md Include your custom transformer in the `transformers:` list when defining your `Spark.Dsl.Extension`. This ensures the transformation is applied during DSL compilation. ```elixir use Spark.Dsl.Extension, sections: [@fields], transformers: [ MyLibrary.Validator.Transformers.AddId ] ``` -------------------------------- ### Asserting No DSL Errors Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md Use `refute_dsl_errors/1` to assert that a do-block produces no Spark DSL errors. This is useful for happy-path tests. The helper returns `:ok` on success and fails with a message listing offending modules and their errors if any are found. ```elixir test "valid configuration is accepted" do refute_dsl_errors do defmodule Elixir.GoodPerson do use MyApp.PersonValidator fields do field :email, :string end end end end ``` -------------------------------- ### Spark DSL and Validation Implementation Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This Elixir module defines a Spark DSL and implements a 'validate/2' function. It handles missing required fields and delegates field-by-field validation. ```elixir defmodule MyLibrary.Validator do use Spark.Dsl, default_extensions: [ extensions: [MyLibrary.Validator.Dsl] ] def validate(module, data) do fields = MyLibrary.Validator.Info.fields(module) required = MyLibrary.Validator.Info.fields_required!(module) case Enum.reject(required, &Map.has_key?(data, &1)) do [] -> validate_fields(fields, data) missing_required_fields -> {:error, :missing_required_fields, missing_required_fields} end end defp validate_fields(fields, data) do Enum.reduce_while(fields, {:ok, %{}}, fn field, {:ok, acc} -> case Map.fetch(data, field.name) do {:ok, value} -> case validate_value(field, value) do {:ok, value} -> {:cont, {:ok, Map.put(acc, field.name, value)}} :error -> {:halt, {:error, :invalid, field.name}} end :error -> {:cont, {:ok, acc}} end end) end defp validate_value(field, value) do with true <- type_check(field, value), true <- check(field, value) do {:ok, transform(field, value)} else _ -> :error end end defp type_check(%{type: :string}, value) when is_binary(value) do true end defp type_check(%{type: :integer}, value) when is_integer(value) do true end defp type_check(_, _), do: false defp check(%{check: check}, value) when is_function(check, 1) do check.(value) end defp check(_, _), do: true defp transform(%{transform: transform}, value) when is_function(transform, 1) do transform.(value) end defp transform(_, value), do: value end ``` -------------------------------- ### Defining Modules for Top-Level Resolution Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md When defining modules inside Spark's testing helpers, use the `Elixir.X` form to ensure top-level resolution. This prevents aliasing issues where a bare alias resolves against the surrounding test module's scope instead of the top level. ```elixir errors = dsl_errors do defmodule Elixir.BadPerson do # <-- note the Elixir. prefix ... end end assert [{BadPerson, _}] = errors # <-- bare alias resolves to Elixir.BadPerson, matches ``` -------------------------------- ### Implement a Transformer Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/writing-extensions.md Define a transformer to modify the DSL state during compilation. Transformers can read and modify any part of the DSL state and can declare dependencies on other transformers. ```elixir defmodule MyApp.Transformers.SetDefaults do use Spark.Dsl.Transformer def transform(dsl_state) do name = Spark.Dsl.Transformer.get_option(dsl_state, [:my_section], :name) dsl_state = Spark.Dsl.Transformer.persist(dsl_state, :name, name || :default) {:ok, dsl_state} end def after?(MyApp.Transformers.EarlierTransformer), do: true def after?(_), do: false end ``` -------------------------------- ### Asserting a Deprecated Option Warning Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md Use `assert_dsl_warning/2` to capture and assert on warnings emitted by a verifier. The warning payload is normalized to a `{message, location | nil}` tuple. ```elixir test "deprecated option emits a warning" do {message, _location} = assert_dsl_warning {_, _} do defmodule Elixir.UsesDeprecatedOption do use MyApp.Validator fields do field :legacy_option, :string end end end assert message =~ "deprecated" end ``` -------------------------------- ### Spark Project Dependency Source: https://github.com/ash-project/spark/blob/main/README.md Adds the 'spark' package as a dependency in an Elixir project's mix.exs file. Ensure this is included for using the Spark framework. ```elixir def deps do [ {:spark, "~> 2.3"} ] end ``` -------------------------------- ### Include Location in DslErrors Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Best practice for creating DslErrors: always include location information when available, retrieved based on error context (section, option, or entity). ```elixir # Get the appropriate annotation for your error context location = case error_type do :section_error -> Spark.Dsl.Transformer.get_section_anno(dsl_state, path) :option_error -> Spark.Dsl.Transformer.get_opt_anno(dsl_state, path, option_name) :entity_error -> entity = Enum.at(entities, entity_index) Spark.Dsl.Entity.anno(entity) end {:error, Spark.Error.DslError.exception( message: "Clear error description", path: path, module: module, location: location )} ``` -------------------------------- ### Explicitly Setting Test Collector in Spawned Process Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md When using `Task.async` or other process spawning mechanisms, ensure the Spark test collector is aware of the parent process by explicitly setting the collector flag within the spawned process. ```elixir parent = self() Task.async(fn -> Process.put({Spark.Dsl, :test_collector}, parent) defmodule Elixir.SomeModule do # ... end end) |> Task.await() ``` -------------------------------- ### Access Entity and Property Annotations Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md Retrieve source location details for an entity and its specific properties when the entity struct includes `__spark_metadata__`. ```elixir # Access annotations entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section]) Enum.each(entities, fn entity -> if entity_anno = Spark.Dsl.Entity.anno(entity) do line = :erl_anno.location(entity_anno) file = :erl_anno.file(entity_anno) |> to_string() IO.puts("Entity defined at #{file}:#{line}") end if name_anno = Spark.Dsl.Entity.property_anno(entity, :name) do line = :erl_anno.location(name_anno) file = :erl_anno.file(name_anno) |> to_string() IO.puts("Entity name property defined at #{file}:#{line}") end end) ``` -------------------------------- ### Define a Spark DSL Extension Source: https://github.com/ash-project/spark/blob/main/documentation/tutorials/get-started-with-spark.md This snippet defines a Spark DSL extension for creating validators. It uses Spark's builder API to define sections, entities, and fields, including their schemas and validation rules. The `__spark_metadata__` field is essential for Spark entities. ```elixir defmodule MyLibrary.Validator.Dsl do alias Spark.Builder.{Entity, Field, Section} defmodule Field do # The __spark_metadata__ field is required for Spark entities # It stores source location information for better error messages and tooling defstruct [:name, :type, :transform, :check, :__spark_metadata__] end @field Entity.new(:field, Field, describe: "A field that is accepted by the validator", args: [:name, :type], schema: [ Field.new(:name, :atom, required: true, doc: "The name of the field"), Field.new(:type, {:one_of, [:integer, :string]}, required: true, doc: "The type of the field" ), Field.new(:check, {:fun, 1}, doc: "A function that can be used to check if the value is valid after type validation." ), Field.new(:transform, {:fun, 1}, doc: "A function that will be used to transform the value after successful validation" ) ] ) |> Entity.build!() @fields Section.new(:fields, describe: "Configure the fields that are supported and required", schema: [ Field.new(:required, {:list, :atom}, doc: "The fields that must be provided for validation to succeed" ) ], entities: [@field] ) |> Section.build!() use Spark.Dsl.Extension, sections: [@fields] end ``` -------------------------------- ### Check OTP Version for End Location Tracking Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md This snippet demonstrates how to check if the `end_location` function is exported by the `:erl_anno` module, which is necessary for tracking end locations in Spark. This check is crucial for ensuring compatibility with OTP 28+. ```elixir if function_exported?(:erl_anno, :end_location, 1) do end_location = :erl_anno.end_location(anno) # Use end location for precise span information end ``` -------------------------------- ### Define a Spark DSL Fragment Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/split-up-large-dsls.md Define a fragment for a DSL, specifying the DSL it belongs to and its configuration. This fragment configures the data layer for an Ash.Resource. ```elixir defmodule MyApp.Accounts.User.Fragments.DataLayer do use Spark.Dsl.Fragment, of: Ash.Resource, data_layer: AshPostgres.DataLayer postgres do table "users" repo MyApp.Repo ... end end ``` -------------------------------- ### Collecting All DSL Errors Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md Use `dsl_errors/1` to collect all Spark DSL errors produced by a do-block into a list. The output format is `[{module, [%Spark.Error.DslError{}]}, ...]`, ordered by definition. This is useful for comparing error counts or asserting on multiple distinct errors. ```elixir errors = dsl_errors do defmodule Elixir.HasTwoIssues do # ... end end assert [{HasTwoIssues, errors_list}] = errors assert length(errors_list) == 2 ``` -------------------------------- ### Asserting a Specific DSL Error Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/test-spark-verifiers.md Use `assert_dsl_error/2` to match a specific Spark DSL error pattern. This helper runs the provided do-block, captures the first matching error, and returns it for further assertions. The pattern matching uses `match?/2`, supporting any valid Elixir pattern. ```elixir defmodule MyApp.PersonValidatorTest do use ExUnit.Case, async: true import Spark.Test test "rejects an invalid email" do err = assert_dsl_error %Spark.Error.DslError{path: [:fields, :email]} do defmodule Elixir.BadPerson do use MyApp.PersonValidator fields do field :email, :string do check &String.contains?(&1, "@") end end end end assert err.message =~ "invalid email" end end ``` -------------------------------- ### Define Entity with Metadata Field Source: https://github.com/ash-project/spark/blob/main/documentation/how_to/use-source-annotations.md To directly access annotations on entities, their struct definition must include the `__spark_metadata__` field. ```elixir defmodule MyEntity do defstruct [ :name, :__spark_metadata__ # Required for annotation access ] end @my_entity %Spark.Dsl.Entity{ name: :my_entity, target: MyEntity, schema: [ name: [type: :atom, required: true] ] } ```