{"id":18319846,"url":"https://github.com/bitwalker/strukt","last_synced_at":"2025-04-07T14:14:28.658Z","repository":{"id":43783587,"uuid":"375179102","full_name":"bitwalker/strukt","owner":"bitwalker","description":"Extends defstruct with schemas, changeset validation, and more","archived":false,"fork":false,"pushed_at":"2024-04-17T03:42:31.000Z","size":62,"stargazers_count":79,"open_issues_count":6,"forks_count":8,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-31T12:06:24.743Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bitwalker.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-09T00:23:12.000Z","updated_at":"2025-02-10T14:34:51.000Z","dependencies_parsed_at":"2024-12-24T06:11:15.449Z","dependency_job_id":"c73c3f76-685f-43af-b727-e1403ce8fcd0","html_url":"https://github.com/bitwalker/strukt","commit_stats":{"total_commits":30,"total_committers":5,"mean_commits":6.0,"dds":0.5,"last_synced_commit":"b2b853256b0360599d67ca9f44b361c2a2a75beb"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwalker%2Fstrukt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwalker%2Fstrukt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwalker%2Fstrukt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwalker%2Fstrukt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bitwalker","download_url":"https://codeload.github.com/bitwalker/strukt/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247666015,"owners_count":20975788,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T18:14:28.320Z","updated_at":"2025-04-07T14:14:28.612Z","avatar_url":"https://github.com/bitwalker.png","language":"Elixir","readme":"# Strukt\n\nStrukt provides an extended `defstruct` macro which builds on top of `Ecto.Schema`\nand `Ecto.Changeset` to remove the boilerplate of defining type specifications,\nimplementing validations, generating changesets from parameters, JSON serialization,\nand support for autogenerated fields.\n\nThis builds on top of Ecto embedded schemas, so the same familiar syntax you use today\nto define schema'd types in Ecto, can now be used to define structs for general purpose\nusage.\n\nThe functionality provided by the `defstruct` macro in this module is strictly a superset\nof the functionality provided both by `Kernel.defstruct/1`, as well as `Ecto.Schema`. If\nyou import it in a scope where you use `Kernel.defstruct/1` already, it will not interfere.\nLikewise, the support for defining validation rules inline with usage of `field/3`, `embeds_one/3`,\netc., is strictly additive, and those additions are stripped from the AST before `field/3`\nand friends ever see it.\n\n## Installation\n\n``` elixir\ndef deps do\n  [\n    {:strukt, \"~\u003e 0.3\"}\n  ]\nend\n```\n\n## Example\n\nThe following is an example of using `defstruct/1` to define a struct with types, autogenerated\nprimary key, and inline validation rules.\n\n``` elixir\ndefmodule Person do\n  use Strukt\n\n  @derives [Jason.Encoder]\n  @primary_key {:uuid, Ecto.UUID, autogenerate: true}\n  @timestamps_opts [autogenerate: {NaiveDateTime, :utc_now, []}]\n\n  defstruct do\n    field :name, :string, required: true\n    field :email, :string, format: ~r/^.+@.+$/\n\n    timestamps()\n  end\nend\n```\n\nAnd an example of how you would create and use this struct:\n\n``` elixir\n# Creating from params, with autogeneration of fields\niex\u003e {:ok, person} = Person.new(name: \"Paul\", email: \"bitwalker@example.com\")\n...\u003e person\n%Person{\n  uuid: \"d420aa8a-9294-4977-8b00-bacf3789c702\",\n  name: \"Paul\",\n  email: \"bitwalker@example.com\",\n  inserted_at: ~N[2021-06-08 22:21:23.490554],\n  updated_at: ~N[2021-06-08 22:21:23.490554]\n}\n\n# Validation (Create)\niex\u003e {:error, %Ecto.Changeset{valid?: false, errors: errors}} = Person.new(email: \"bitwalker@example.com\")\n...\u003e errors\n[name: {\"can't be blank\", [validation: :required]}]\n\n# Validation (Update)\niex\u003e {:ok, person} = Person.new(name: \"Paul\", email: \"bitwalker@example.com\")\n...\u003e {:error, %Ecto.Changeset{valid?: false, errors: errors}} = Person.change(person, email: \"foo\")\n...\u003e errors\n[email: {\"has invalid format\", [validation: :format]}]\n\n# JSON Serialization/Deserialization\n...\u003e person == person |\u003e Jason.encode!() |\u003e Person.from_json()\ntrue\n```\n\n## Validation\n\nThere are a few different ways to express and customize validation rules for a struct.\n\n* Inline (as shown above, these consist of the common validations provided by `Ecto.Changeset`)\n* Validators (with module and function variants, as shown below)\n* Custom (by overriding the `validate/1` callback)\n\nThe first two are the preferred method of expressing and controlling validation of a struct, but\nif for some reason you prefer a more manual approach, overriding the `validate/1` callback is an\noption available to you and allows you to completely control validation of the struct.\n\nNOTE: Be aware that if you override `validate/1` without calling `super/1` at some point in your\nimplementation, none of the inline or module/function validators will be run. It is expected that\nif you are overriding the implementation, you are either intentionally disabling that functionality,\nor are intending to delegate to it only in certain circumstances.\n\n### Module Validators\n\nThis is the primary method of implementing reusable validation rules:\n\nThere are two callbacks, `init/1` and `validate/2`. You can choose to omit the\nimplementation of `init/1` and a default implementation will be provided for you.\nThe default implementation returns whatever it is given as input. Whatever is returned\nby `init/1` is given as the second argument to `validate/2`. The `validate/2` callback\nis required.\n\n```elixir\ndefmodule MyValidator.ValidPhoneNumber do\n  use Strukt.Validator\n\n  @pattern ~r/^(\\+1 )[0-9]{3}-[0-9]{3}-[0-9]{4}$/\n\n  @impl true\n  def init(opts), do: Enum.into(opts, %{})\n\n  @impl true\n  def validate(changeset, %{fields: fields}) do\n    Enum.reduce(fields, changeset, fn field, cs -\u003e\n      case fetch_change(cs, field) do\n        :error -\u003e\n          cs\n\n        {:ok, value} when value in [nil, \"\"] -\u003e\n          add_error(cs, field, \"phone number cannot be empty\")\n\n        {:ok, value} when is_binary(value) -\u003e\n          if value =~ @pattern do\n            cs\n          else\n            add_error(cs, field, \"invalid phone number\")\n          end\n\n        {:ok, _} -\u003e\n          add_error(cs, field, \"expected phone number to be a string\")\n      end\n    end)\n  end\nend\n```\n\n### Function Validators\n\nThese are useful for ad-hoc validators that are specific to a single struct and aren't likely\nto be useful in other contexts. The function is expected to received two arguments, the first\nis the changeset to be validated, the second any options passed to the `validation/2` macro:\n\n```elixir\ndefmodule File do\n  use Strukt\n\n  @allowed_content_types []\n\n  defstruct do\n    field :filename, :string, required: true\n    field :content_type, :string\n    field :content, :binary\n  end\n\n  validation :validate_filename_matches_content_type, @allowed_content_types\n\n  defp validate_filename_matches_content_type(changeset, allowed) do\n    # ...\n  end\nend\n```\n\nAs with module validators, the function should always return an `Ecto.Changeset`.\n\n### Conditional Rules\n\nYou may express validation rules that apply only conditionally using guard clauses. For example,\nextending the example above, we could validate that the filename and content type match only when\neither of those fields are changed:\n\n```elixir\n  # With options\n  validation :validate_filename_matches_content_type, @allowed_content_types\n    when is_map_key(changeset.changes, :filename) or is_map_key(changeset.changes, :content_type)\n\n  # Without options\n  validation :validate_filename_matches_content_type\n    when is_map_key(changeset.changes, :filename) or is_map_key(changeset.changes, :content_type)\n```\n\nBy default validation rules have an implicit guard of `when true` if one is not explicitly provided.\n\n## Custom Fields\n\nUsing the `:source` option allows you to express that a given field may be provided as a parameter\nusing a different naming scheme than is used in idiomatic Elixir code (i.e. snake case):\n\n``` elixir\ndefmodule Person do\n  use Strukt\n\n  @derives [Jason.Encoder]\n  @primary_key {:uuid, Ecto.UUID, autogenerate: true}\n  @timestamps_opts [autogenerate: {NaiveDateTime, :utc_now, []}]\n\n  defstruct do\n    field :name, :string, required: true, source: :NAME\n    field :email, :string, format: ~r/^.+@.+$/\n\n    timestamps()\n  end\nend\n\n# in iex\niex\u003e {:ok, person} = Person.new(%{NAME: \"Ivan\", email: \"ivan@example.com\"})\n...\u003e person\n%Person{\n  uuid: \"f8736f15-bfdc-49bd-ac78-9da514208464\",\n  name: \"Ivan\",\n  email: \"ivan@example.com\",\n  inserted_at: ~N[2021-06-08 22:21:23.490554],\n  updated_at: ~N[2021-06-08 22:21:23.490554]\n}\n```\n\nNOTE: This does not affect serialization/deserialization via `Jason.Encoder` when derived.\n\nFor more, see the [usage docs](https://hexdocs.pm/strukt/usage.html)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitwalker%2Fstrukt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitwalker%2Fstrukt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitwalker%2Fstrukt/lists"}