# NAME

Dancer2::Plugin::QuickORM - Adds QuickORM syntactic sugar to Dancer2

# VERSION

version 1.0000

# SYNOPSIS

    package MyApp;
    use Dancer2;
    use Dancer2::Plugin::QuickORM;

    set plugins => {
       QuickORM => {
          default => {
             class => 'MyApp::ORM',
          },
       },
    };

    # 'widget' table -> singular/plural keywords widget()/widgets()
    get '/widget/:id' => sub {
       return widget( route_parameters->get('id') );
    };

    get '/widgets' => sub {
       return [ widgets( { color => 'blue' } ) ];
    };

    # get at the raw ORM connection directly
    get '/stats' => sub {
       return { row_count => orm()->handle('widget')->all };
    };

# DESCRIPTION

`Dancer2::Plugin::QuickORM` looks at the ORM(s) you've configured with [DBIx::QuickORM](https://metacpan.org/pod/DBIx%3A%3AQuickORM),
walks every table in their schema(s), and generates a Dancer2 keyword (i.e. an
importable function) for each one, so routes can query it without any
boilerplate.

For each configured ORM name (see ["CONFIGURATION AND ENVIRONMENT"](#configuration-and-environment)), the
plugin's `BUILD` step:

1. Loads the configured `class` and calls `$class->orm($name)` to obtain
a connection.
2. Iterates `$orm->schema($name)->tables` and, for each table, derives a
singular and a plural keyword name from the table name (splitting
`StudlyCaps`/`CamelCase` into words and singularising/pluralising with
[Lingua::EN::Inflect::Phrase](https://metacpan.org/pod/Lingua%3A%3AEN%3A%3AInflect%3A%3APhrase)).
3. Registers those keywords, plus schema-prefixed variants
(`<name>_<singular>` and `<name>_<plural>`), so a table is always
reachable unambiguously even if more than one configured ORM exposes a
table of the same name.

## Generated keywords

If a table's singular and plural forms differ (the common case, e.g.
`widget`/`widgets`), two keywords are generated:

- `singular($id)`

    Fetches the single row with that primary key. Croaks if called with no
    argument, or with a hashref (which is a search, not a primary key).

- `plural($where)`

    Runs a search. Called with no arguments, returns every row. Called with a
    hashref, runs it as search conditions against the table. Croaks if given
    anything other than a hashref.

If a table's singular and plural forms are identical (e.g. `moose`), a
single "smart" keyword is generated instead, which inspects the shape of
its argument to decide what you meant:

- a hashref is treated as search conditions (like the plural form above);
- `undef` (i.e. no argument) croaks, since there's no sensible default;
- anything else (a plain scalar, or an arrayref) is treated as a
primary key value and looked up like the singular form above;
- any other kind of reference (coderef, scalarref, etc.) croaks,
since it's neither a valid search nor a valid primary key.

## Keyword collisions

Two different kinds of collision are handled, and handled differently:

- Two configured ORMs derive the same bare keyword

    The bare keyword (e.g. `widget`) becomes ambiguous: calling it croaks,
    telling you to use the schema-prefixed form (e.g. `default_widget` or
    `second_widget`) instead. The schema-prefixed forms always work.

- A derived keyword collides with an existing keyword

    If the bare keyword name is already in use (either because it's a core
    Dancer2 DSL keyword, e.g. `session` or `response_header`, or because
    this plugin has already registered it itself - its own `orm` keyword,
    see below), the bare form is silently skipped so the existing keyword
    keeps working. The schema-prefixed form is still registered and
    reachable.

# SUBROUTINES/METHODS

## orm

    orm();          # the connection configured as 'default'
    orm($name);     # the connection configured under $name

Returns the raw ORM connection object for the given configured name
(defaults to `'default'` when no name is given). Croaks if `$name` was
not configured.

This is the one keyword the plugin always registers itself (via
`plugin_keywords`), regardless of what tables are configured; use it when
you need something the generated per-table keywords don't cover.

## BUILD

Runs once when the plugin is instantiated. Reads the plugin configuration,
loads each configured ORM class, and generates the per-table keywords
described in ["DESCRIPTION"](#description). Not intended to be called directly.

# ATTRIBUTES

This plugin exposes no public attributes of its own. All of its behaviour
is driven by the `plugins->QuickORM` section of your Dancer2
configuration; see ["CONFIGURATION AND ENVIRONMENT"](#configuration-and-environment).

# DIAGNOSTICS

- `ORM '%s' is missing 'class' in the configuration`

    A configured ORM name has no `class` key. Every entry under
    `plugins->QuickORM` must specify one.

- `Failed to load ORM class '%s': %s`

    The configured `class` could not be `require`d; the underlying error is
    appended.

- `ORM '%s' is not defined in the configuration`

    `orm($name)` was called with a name that wasn't configured.

- `'%s' requires a primary key value`

    A singular (or smart) keyword was called with no argument.

- `'%s' expects a single primary key value, not a hashref of search conditions`

    A singular keyword was called with a hashref; use the plural (search)
    keyword instead.

- `'%s' expects a hashref of search conditions`

    A plural keyword was called with a non-hashref, non-undef argument.

- `'%s' requires a primary key value or a hashref of search conditions`

    A smart keyword (identical singular/plural table) was called with no
    argument.

- `'%s' does not accept a reference of type '%s'`

    A smart keyword was called with a reference that's neither a hashref
    (search) nor usable as a primary key.

- `%s is ambiguous; use <schema>_%s instead!`

    The bare keyword is generated by more than one configured ORM; call the
    schema-prefixed form instead.

# CONFIGURATION AND ENVIRONMENT

Configure one or more named ORMs under `plugins->QuickORM` in your
Dancer2 config:

    plugins:
      QuickORM:
        default:
          class: MyApp::ORM
          dsn: "dbi:Pg:dbname=myapp"
        reporting:
          class: MyApp::ORM
          dsn: "dbi:Pg:dbname=myapp_reporting"

Each key under `QuickORM` is an ORM name (`default` is used when
`orm()` is called without an argument); each value is a hashref that must
contain a `class`, plus whatever other keys your ORM class needs.

Every key/value pair in a given ORM's configuration (including `class`
itself) is also copied into `%ENV` as `QuickORM_<name>_<key>`, and
the name itself is set as `QuickORM_<name>_name`, so the ORM class's
`orm()` constructor can pick up its configuration from the environment if
it prefers that to being passed arguments directly.

The configured `class` must implement the following [DBIx::QuickORM](https://metacpan.org/pod/DBIx%3A%3AQuickORM) features:

    $class->orm($name)                     # -> connection object
    $orm->schema($name)->tables            # -> list of table objects
    $table->name                           # -> table name string
    $orm->handle($table_name)              # -> handle object
    $handle->by_id($id)                    # -> single row (hashref) or undef
    $handle->where($hashref)->all          # -> list of matching rows
    $handle->all                           # -> list of every row

# DEPENDENCIES

- [Dancer2](https://metacpan.org/pod/Dancer2), naturally.
- [DBIx::QuickORM](https://metacpan.org/pod/DBIx%3A%3AQuickORM)
- [Carp](https://metacpan.org/pod/Carp),
- [Lingua::EN::Inflect::Phrase](https://metacpan.org/pod/Lingua%3A%3AEN%3A%3AInflect%3A%3APhrase)

# BUGS AND LIMITATIONS

Table names that singularise/pluralise to the same word as a Dancer2 core
DSL keyword or another already-registered keyword are only reachable via
their schema-prefixed keyword; see ["Keyword collisions"](#keyword-collisions).

For tables whose singular and plural forms are identical, an arrayref
argument is treated as a primary key value (passed straight through to the
backend) rather than being rejected outright, since it isn't a hashref and
isn't some other clearly-invalid reference type; whether that's a
meaningful primary key value is left to the configured ORM class.

Please report any bugs or feature requests through the issue tracker for
this distribution.

# SEE ALSO

[Dancer2](https://metacpan.org/pod/Dancer2), [Dancer2::Plugin](https://metacpan.org/pod/Dancer2%3A%3APlugin), [DBIx::QuickORM](https://metacpan.org/pod/DBIx%3A%3AQuickORM)

# AUTHOR

D Ruth Holloway <ruth@hiruthie.me>

# COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by D Ruth Holloway.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

# POD ERRORS

Hey! **The above document had some coding errors, which are explained below:**

- Around line 264:

    You forgot a '=back' before '=head1'
