Packages

Food allergen information and standardized SVG icons for food labeling.

Current section

Files

Jump to
Raw

README.md

# FoodAllergens

![FoodAllergens logo](assets/logo.svg)

A comprehensive Elixir library providing detailed information about food allergens, including food examples and standardized SVG icons. Based on the Erudus icons collection, this library helps applications display allergen information consistently and accurately.

![CI](https://img.shields.io/github/actions/workflow/status/eiger-code/ex-food-allergens/elixir.yml?branch=main)
![License: MIT](https://img.shields.io/badge/license-MIT-blue)
![Elixir: ~> 1.18](https://img.shields.io/badge/Elixir-~%3E%201.18-4B275F)
![Erlang/OTP: 27](https://img.shields.io/badge/OTP-27-6E4A7E)

- πŸ₯œ **Allergen data** β€” 14 EU allergens, each with a display name, description, and food examples
- 🎨 **SVG icon set** β€” `:standard`, `:circle`, and `:free_from` variants for every allergen
- 🌱 **Dietary label icons** β€” `:vegetarian` and `:vegan`, in `:standard` and `:circle` shapes
- πŸͺΆ **Zero runtime dependencies**
- πŸ“˜ **Type safety** β€” full type specifications and documentation

## Contents

- [Installation](#installation)
- [Supported allergens](#supported-allergens)
- [Accuracy and scope](#accuracy-and-scope)
- [Usage](#usage)
- [Configuration](#configuration)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)

## Installation

Add `food_allergens` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:food_allergens, "~> 0.2.0"}
  ]
end
```

Then run `mix deps.get`.

## Supported allergens

This is the normative order returned by `FoodAllergens.list/0`:

- Celery
- Gluten / cereals containing gluten (wheat, rye, barley, oats, spelt, kamut)
- Milk
- Eggs
- Fish
- Peanuts
- Soya
- Crustaceans
- Mustard
- Sesame
- Lupin
- Sulphur Dioxide (SOβ‚‚)
- Molluscs
- Tree Nuts

## Accuracy and scope

This is an informational convenience dataset, not legal or medical advice.

The list reflects the 14 allergens that must be declared in the European Union under [Regulation (EU) No 1169/2011](https://eur-lex.europa.eu/eli/reg/2011/1169/oj), Annex II, and it covers that jurisdiction only: other regimes declare different sets (the United States, for example, declares a "Big 9").

The `food_examples` are illustrative and non-exhaustive, and the data does not model the declaration exemptions in that Annex β€” for example, fully refined soybean oil, and fish gelatine used as a fining agent in beer and wine, are exempt from declaration.

Verify against the current regulation and your product's actual formulation before relying on this data for labelling. `FoodAllergens.Allergens.metadata/0` (or `FoodAllergens.metadata/0`) reports the source, a link to the regulation, the jurisdiction, and the date the list was last reviewed.

## Usage

### API at a glance

| Function | Returns |
| --- | --- |
| `FoodAllergens.list/0` | every allergen, in the documented order |
| `FoodAllergens.get/1` | one allergen by name, or `nil` |
| `FoodAllergens.metadata/0` | data provenance: source, regulation, jurisdiction, and last review date |
| `FoodAllergens.get_icon/2,3` | SVG markup for an allergen (`:standard`, `:circle`, `:free_from`) |
| `FoodAllergens.get_dietary_icon/2,3` | SVG markup for a dietary label (`:vegetarian`, `:vegan`; `:standard`, `:circle`) |
| `FoodAllergens.Icons.svg_path/2`, `dietary_svg_path/2`, `default_path/0` | filesystem paths to the bundled icons |

See <https://hexdocs.pm/food_allergens> for full documentation.

### Listing and looking up allergens

```elixir
# Every allergen, in the documented order
allergens = FoodAllergens.list()
Enum.each(allergens, &IO.puts(&1.display_name))

# One allergen, or nil when the name is unknown
peanuts = FoodAllergens.get("peanuts")
# %FoodAllergens.Allergen{
#   name: "peanuts",
#   display_name: "Peanuts",
#   description: "Peanuts and peanut-derived products",
#   food_examples: ["peanuts", "peanut butter", "peanut oil", "some candies"]
# }

FoodAllergens.get("nonexistent")
# nil
```

### Getting SVG icons

```elixir
# Standard is the default variant; :circle and :free_from are the alternatives
FoodAllergens.get_icon("peanuts")
FoodAllergens.get_icon("peanuts", :circle)
FoodAllergens.get_icon("peanuts", :free_from)

# Dietary labels take a shape, not an allergen name
FoodAllergens.get_dietary_icon(:vegetarian)
FoodAllergens.get_dietary_icon(:vegan, :circle)
```

- Every call returns a complete SVG string.
- Requests for an unknown name, an unknown variant, or a name that is not a bare file name return `nil`. After surrounding whitespace is trimmed, a name must match `[A-Za-z0-9_-]+`.
- `:free_from` exists for every allergen. The dietary labels (`:vegetarian`, `:vegan`) describe a label rather than an allergen, so they use `get_dietary_icon/2` with a `:standard` or `:circle` shape and never take an allergen name.
- Bundled icons are embedded at compile time, so resolving one performs no file I/O and nothing is cached. See [Configuration](#configuration) for how a configured directory changes that.

## Configuration

`FoodAllergens.get_icon/2` and `FoodAllergens.get_dietary_icon/2` resolve each icon from the highest-precedence source that provides it:

| Source | Applies | Read |
| --- | --- | --- |
| `:icons_path` per-call option | `get_icon/3` and `get_dietary_icon/3` | on every call |
| `:svg_icons_path` application env | when no per-call option is given | on every call |
| Bundled icons | fallback, and always for the path helpers | never; embedded at compile time |

Set the application-wide directory in `config/config.exs`:

```elixir
config :food_allergens, :svg_icons_path, "path/to/your/custom/icons"
```

Or pass `:icons_path` as the optional last argument to `get_icon/3` or `get_dietary_icon/3`. It takes precedence over `:svg_icons_path` for that call, which lets one node serve several icon sets and lets tests avoid global state:

```elixir
FoodAllergens.get_icon("peanuts", :circle, icons_path: "path/to/your/custom/icons")
FoodAllergens.get_dietary_icon(:vegan, icons_path: "path/to/your/custom/icons")

# Serve the bundled icons even when :svg_icons_path is set
FoodAllergens.get_icon("peanuts", icons_path: nil)
```

Because `variant` and `shape` have defaults, the options may also be given in their place: `FoodAllergens.get_icon("peanuts", icons_path: dir)`.

Resolution rules:

- A configured directory overrides icon **contents** only, one file at a time: any icon it does not contain falls back to the embedded one, so you can overlay just the icons you want to replace.
- When `:svg_icons_path` is not configured (or is blank or invalid), every icon resolves to the bundled set.
- Icons supplied by a custom directory or a per-call `:icons_path` are reachable through `FoodAllergens.get_icon/2` (and `get_dietary_icon/2`, plus their `/3` forms) only.
- The path helpers are narrower by design. `FoodAllergens.Icons.svg_path/2`, `dietary_svg_path/2`, and `default_path/0` always describe the **bundled** icons and never consult `:svg_icons_path` or a per-call `:icons_path`. `default_path/0` is resolved with `Application.app_dir/2`, so it works regardless of the host application's working directory. Serve `default_path/0` for static assets: it always contains a file for every icon, so a static server never 404s on an icon that `FoodAllergens.get_icon/2` can resolve.

When using a custom path, maintain the expected directory structure:

```text
your_custom_path/
β”œβ”€β”€ standard/
β”‚   β”œβ”€β”€ peanuts.svg
β”‚   └── milk.svg
β”œβ”€β”€ circle/
β”‚   β”œβ”€β”€ peanuts.svg
β”‚   └── milk.svg
β”œβ”€β”€ free_from/
β”‚   β”œβ”€β”€ peanuts.svg
β”‚   └── milk.svg
└── dietary/
    β”œβ”€β”€ vegetarian.svg
    β”œβ”€β”€ vegetarian_circle.svg
    β”œβ”€β”€ vegan.svg
    └── vegan_circle.svg
```

## Development

### Tests and checks

```bash
mix test         # Run the test suite
mix check        # Everything: format, compile, test, credo, dialyzer
mix credo        # Run Credo for code quality checks
mix dialyzer     # Run Dialyzer for static analysis
mix format       # Format code
```

### Toolchain

Elixir and Erlang/OTP are pinned in `.tool-versions` to the oldest supported combination β€” Elixir 1.18.4 on Erlang/OTP 27.3.4 β€” so the library is exercised at the floor declared by `elixir: "~> 1.18"` in `mix.exs`.

### Documentation

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc):

```bash
mix docs
```

Once published, the docs can be found at <https://hexdocs.pm/food_allergens>.

## Contributing

Bug reports and pull requests are welcome at <https://github.com/eiger-code/ex-food-allergens>.

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Run the test suite and quality checks
6. Submit a pull request

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

The SVG files under `priv/icons` are from the [Erudus icons collection](https://github.com/Erudus/erudus-icons) and are MIT licensed as well.