# NAME

DBIx::Class::PseudoEnum - Schema-based enumerations independent of database

# VERSION

version 1.0003

# SYNOPSIS

    # In your Schema::Result class:
    __PACKAGE__->load_components('PseudoEnum');

    # Load your enumerations into source_info directly
    __PACKAGE__->table('contraption');
    __PACKAGE__->source_info(
    {
       enumerations => { 'status' => [qw/Sold Packaged Shipped/] }
    }

    # Or use the handy-dandy methods
    # (leave off the __PACKAGE__ if you're using DBIx::Class::Candy)
    __PACKAGE__->table('doodad');
    __PACKAGE__->enumerate( 'status', [qw/Ordered In-Stock Out-Of-Stock/] );
    __PACKAGE__->enumerate( 'color',  [qw/Black Blue Green Red/] );

    # Properly handle value collisions in the same table
    # (both fields here could create an 'is_blue' method!)
    __PACKAGE__->table('doohickey');
    __PACKAGE__->enumerations_use_column_names();
    __PACKAGE__->enumerate( 'field1', [qw/One Two Three Four Blue/] );
    __PACKAGE__->enumerate( 'field2', [qw/BLUE RED GREEN/] );

    # Later, in your application:
    # On Results:
    $doodad->is_ordered;                      # Boolean, true if doodad.status == 'Ordered'
    $doodad->is_blue;                         # Boolean, true if doodad.color == 'Blue'
    $doodad->update({ status => 'Dunno'});    # croaks!
    $doodad->update({ status => 'ordered' }); # croaks!
    $doodad->update({ color => 'Black' });    # okay!
    # The module will try to pass this on to the rest of the update() method; if the 
    # field is nullable, it'll work.
    $doodad->update({ color => undef });   

    # On ResultSets:
    $doodad_rs->create({ status => 'Dunno' });    # croaks!
    $doodad_rs->create({ status => 'ordered' });  # croaks!
    $doodad_rs->create({ color  => 'Black' });    # okay!
    $doodad_rs->is_blue                           # Returns a ResultSet where doodad.color == 'Blue'

    # With enumerations_use_column_names:
    $doohickey->is_blue                    # "no such method"
    $doohickey->field1_is_blue             # Now it does what you want!

# DESCRIPTION

Enumerations can be a bit of a pain. Not all databases support them equally (or at all), which reduces
the portability of your application. Additionally, there are some 
[philosophical and practical problems](https://chateau-logic.com/content/why-we-should-not-use-enums-databases)
with them. Lookup tables are an alternative, but maybe you don't want to clutter up your DB with single-column
lookup tables.

But searching around the interwebs, no one seems to mind enumerating valid values for a data entity within
the application layer. So that's what this module provides: a way to put the enumeration in the `DBIx::Class`
schema, and have it enforced within the application, invisibly to the DB.

# SUBROUTINES/METHODS

## enumerate( `$field`, `[$value1, $value2,...]`)

This is the brains of the outfit, right here. The field must be a column in your table, and the values must be sent
in as a hashref.  Easy and obvious.

This method spins off methods in your Result and ResultSet classes for each value in your list, of the form
`is_value`, which return a boolean (zero or one) if the current value of the enumerated field is the specified
value. If the field is nullable, you **do not** get an `is_undef` method. Yet. See LIMITATIONS below.

## enumerations\_use\_column\_names()

Calling this function will require the schema to create methods with the column name included.  E.g. instead of 
`is_value`, you get `fieldname_is_value` methods. It only operates on the Result class where you call it.

# DEPENDENCIES

- [Carp](https://metacpan.org/pod/Carp)
- [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass)
- [Modern::Perl](https://metacpan.org/pod/Modern%3A%3APerl)
- [Sub::Quote](https://metacpan.org/pod/Sub%3A%3AQuote)

# BUGS AND LIMITATIONS

Bugs?  What bugs?  (No, really. If you find one, open an issue, please.)

The following limitations are (currently) present:

- **Text columns only!**:

    At present, you may only use this with text-based columns.

- **Collisions**:

    If you have two enumerated fields in a table, and their lower-cased, underscore-punctuated
    values collide, the code will choose the **last** one that you defined with an `enumerate`
    statement. In this instance, you should probably use `enumerations_use_column_names` to force
    column names to be listed.

    If you have multiple enumerated values in a single field that collide on their lower-cased,
    underscore-punctuated values, then **any** of them will respond to test methods:  e.g. if you
    have `BLUE` and `blue` values in an enumeration, then `is_blue` will be true for either one.
    (...but why would you do that?)

- **undef**

    If a field is nullable in the DB and the schema, you do not get an `is_undef` method. Yet.

- **Case-insensitive**

    To make the method name, this module replaces all non-alphanumeric characters with underscores,
    and smashes case on all upper-case letters. This may contribute to collisions (see above).

- **Adding to existing code**

    If you have an application where you add this module's functionality after there is data in
    the table, it **will not** complain about already-existing invalid values in enumerated fields.
    You will not, of course, be able to test for those values, nor set any other record to that
    value, unless you enumerate it.  

- **Error handling**

    If you've got a Row result, and try to update an enumerated field with an invalid value, it'll croak.
    That's probably what you want, but if you have that in, for instance, a [Try::Tiny](https://metacpan.org/pod/Try%3A%3ATiny) block, you
    then have a "dirty" column for your enumerated column, and the next update may mess with you by
    going ahead and \*doing the update to the invalid value\*.  You can do $result->discard\_changes, and not
    have to reload your object.  This isn't a bug, precisely, but it is a known quirk, one that I'd like
    to eradicate.

# ROADMAP

I have these features in mind, going forward.

- Handle non-text columns
- Automatically detect and force collision behavior
- Add an `is_undef` method for nullable fields
- Option flag to make it work with case-sensitive enumerations
- Method to hunt for 'invalid' values in the database and report
- `is_not_value` methods

# ACKNOWLEDGEMENTS

My boss at Clearbuilt really, really dislikes enumerations. Hopefully, this module will make
them a bit easier for him to use.

[Jason Crome](https://metacpan.org/author/CROMEDOME) encourages this sort of craziness fairly
often.

# AUTHOR

D Ruth Holloway <ruth@hiruthie.me>

# COPYRIGHT AND LICENSE

This software is copyright (c) 2023 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.