The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

DBIx::NinjaORM - Flexible Perl ORM for easy transitions from inline SQL to objects.

VERSION

Version 3.1.0

DESCRIPTION

DBIx::NinjaORM was designed with a few goals in mind:

  • Expand objects with data joined from other tables, to do less queries and prevent lazy-loading of ancillary information.

  • Have a short learning curve.

  • Provide advanced caching features and manage cache expiration upon database changes.

  • Allow a progressive introduction of a separate Model layer in a legacy codebase.

SYNOPSIS

Simple example

Let's take the example of a My::Model::Book class that represents a book. You would start My::Model::Book with the following code:

        package My::Model::Book;

        use strict;
        use warnings;

        use base 'DBIx::NinjaORM';

        use DBI;


        sub static_class_info
        {
                my ( $class ) = @_;

                # Retrieve defaults from DBIx::Ninja->static_class_info().
                my $info = $class->SUPER::static_class_info();

                $info->set(
                        {
                                # Set mandatory defaults.
                                table_name       => 'books',
                                primary_key_name => 'book_id',
                                default_dbh      => DBI->connect(
                                        "dbi:mysql:[database_name]:localhost:3306",
                                        "[user]",
                                        "[password]",
                                ),

                                # Add optional information.
                                # Allow filtering SELECTs on books.name.
                                filtering_fields => [ 'name' ],
                        }
                );

                return $info;
        }

        1;

Inheriting with use base 'DBIx::NinjaORM' and creating sub static_class_info (with a default database handle and a table name) are the only two requirements to have a working model.

A more complex model

If you have more than one Model class to create, for example My::Model::Book and My::Model::Library, you probably want to create a single class My::Model to hold the defaults and then inherits from that main class.

        package My::Model;

        use strict;
        use warnings;

        use base 'DBIx::NinjaORM';

        use DBI;
        use Cache::Memcached::Fast;


        sub static_class_info
        {
                my ( $class ) = @_;

                # Retrieve defaults from DBIx::Ninja->static_class_info().
                my $info = $class->SUPER::static_class_info();

                # Set defaults common to all your objects.
                $info->set(
                        {
                                default_dbh => DBI->connect(
                                        "dbi:mysql:[database_name]:localhost:3306",
                                        "[user]",
                                        "[password]",
                                ),
                                memcache    => Cache::Memcached::Fast->new(
                                        {
                                                servers =>
                                                [
                                                        'localhost:11211',
                                                ],
                                        }
                                ),
                        }
                );

                return $info;
        }

        1;

The various classes will then inherit from My::Model, and the inherited defaults will make static_class_info() shorter in the other classes:

        package My::Model::Book;

        use strict;
        use warnings;

        # Inherit from your base model class, not from DBIx::NinjaORM.
        use base 'My::Model';

        sub static_class_info
        {
                my ( $class ) = @_;

                # Retrieve defaults from My::Model.
                my $info = $class->SUPER::static_class_info();

                $info->set(
                        {
                                # Set mandatory defaults for this class.
                                table_name       => 'books',
                                primary_key_name => 'book_id',

                                # Add optional information.
                                # Allow filtering SELECTs on books.name.
                                filtering_fields => [ 'name' ],
                        }
                );

                return $info;
        }

        1;

SUPPORTED DATABASES

This distribution currently supports:

  • SQLite

  • MySQL

  • PostgreSQL

Please contact me if you need support for another database type, I'm always glad to add extensions if you can help me with testing.

SUBCLASSABLE METHODS

DBIx::NinjaORM is designed with inheritance in mind, and you can subclass most of its public methods to extend or alter its behavior.

This group of method covers the most commonly subclassed methods, with examples and use cases.

clone()

Clone the current object and return the clone.

        my $cloned_book = $book->clone();

commit()

Convenience function to insert or update the object.

If the object has a primary key set, update() is called, otherwise insert() is called. If there's an error, the method with croak with relevant error information.

        $book->commit();

Arguments: (none).

get()

Get the value corresponding to an object's field.

        my $book_name = $book->get('name');

This method will croak if you attempt to retrieve a private field. It also detects if the object was retrieved from the database, in which case it has an exhaustive list of the fields that actually exist in the database and it will croak if you attempt to retrieve a field that doesn't exist in the database.

get_current_time()

Return the current time, to use in SQL statements.

        my $current_time = $class->get_current_time( $field_name );

By default, DBIx::NinjaORM assumes that time is stored as unixtime (integer) in the database. If you are using a different field type for created and modified, you can subclass this method to return the current time in a different format.

Arguments:

  • $field_name

    The name of the field that will be populated with the return value.

Notes:

  • The return value of this method will be inserted directly into the database, so you can use NOW() for example, and if you are inserting strings those should be quoted in the subclassed method.

insert()

Insert a row corresponding to the data passed as first parameter, and fill the object accordingly upon success.

        my $book = My::Model::Book->new();
        $book->insert(
                {
                        name => 'Learning Perl',
                }
        );

If you don't need the object afterwards, you can simply do:

        My::Model::Book->insert(
                {
                        name => 'Learning Perl',
                }
        );

This method supports the following optional arguments:

  • overwrite_created

    A UNIX timestamp to be used instead of the current time for the value of 'created'.

  • generated_primary_key_value

    A primary key value, in case the underlying table doesn't have an autoincremented primary key.

  • dbh

    A different database handle than the default one specified in static_class_info(), but it has to be writable.

  • ignore

    INSERT IGNORE instead of plain INSERT.

        $book->insert(
                \%data,
                overwrite_created           => $unixtime,
                generated_primary_key_value => $value,
                dbh                         => $dbh,
                ignore                      => $boolean,
        );

new()

new() has two possible uses:

  • Creating a new empty object

            my $object = My::Model::Book->new();
  • Retrieving a single object from the database.

            # Retrieve by ID.
            my $object = My::Model::Book->new( { id => 3 } )
                    // die 'Book #3 does not exist';
    
            # Retrieve by unique field.
            my $object = My::Model::Book->new( { isbn => '9781449303587' } )
                    // die 'Book with ISBN 9781449303587 does not exist';

When retrieving a single object from the database, the first argument should be a hashref containing the following information to select a single row:

  • id

    The ID for the primary key on the underlying table. id is an alias for the primary key field name.

            my $object = My::Model::Book->new( { id => 3 } )
                    // die 'Book #3 does not exist';
  • A unique field

    Allows passing a unique field and its value, in order to load the corresponding object from the database.

            my $object = My::Model::Book->new( { isbn => '9781449303587' } )
                    // die 'Book with ISBN 9781449303587 does not exist';

    Note that unique fields need to be defined in static_class_info(), in the unique_fields key.

This method also supports the following optional arguments, passed in a hash after the filtering criteria above-mentioned:

  • skip_cache (default: 0)

    By default, if cache is enabled with object_cache_time() in static_class_info(), then new attempts to load the object from the cache first. Setting skip_cache to 1 forces the ORM to load the values from the database.

            my $object = My::Model::Book->new(
                    { isbn => '9781449303587' },
                    skip_cache => 1,
            ) // die 'Book with ISBN 9781449303587 does not exist';
  • lock (default: 0)

    By default, the underlying row is not locked when retrieving an object via new(). Setting lock to 1 forces the ORM to bypass the cache if any, and to lock the rows in the database as it retrieves them.

            my $object = My::Model::Book->new(
                    { isbn => '9781449303587' },
                    lock => 1,
            ) // die 'Book with ISBN 9781449303587 does not exist';

remove()

Delete in the database the row corresponding to the current object.

        $book->remove();

This method accepts the following arguments:

  • dbh

    A different database handle from the default specified in static_class_info(). This is particularly useful if you have separate reader/writer databases.

retrieve_list_nocache()

Dispatch of retrieve_list() when objects should not be retrieved from the cache.

See retrieve_list() for the parameters this method accepts.

set()

Set fields and values on an object.

        $book->set(
                {
                        name => 'Learning Perl',
                        isbn => '9781449303587',
                },
        );

This method supports the following arguments:

  • force

    Set the properties on the object without going through validate_data().

            $book->set(
                    {
                            name => 'Learning Perl',
                            isbn => '9781449303587',
                    },
                    force => 1,
            );

static_class_info()

This methods sets defaults as well as general information for a specific class.

It allows for example indicating what table the objects will be related to, or what database handle to use. See DBIx::NinjaORM::StaticClassInfo for the full list of options that can be set or overridden.

Here's what a typical subclassed static_class_info() would look like:

        sub static_class_info
        {
                my ( $class ) = @_;

                # Retrieve defaults coming from higher in the inheritance chain, up
                # to DBIx::NinjaORM->static_class_info().
                my $info = $class->SUPER::static_class_info();

                # Set or override information.
                $info->set(
                        {
                                table_name       => 'books',
                                primary_key_name => 'book_id',
                                default_dbh      => DBI->connect(
                                        "dbi:mysql:[database_name]:localhost:3306",
                                        "[user]",
                                        "[password]",
                                ),
                        }
                );

                # Return the updated information hashref.
                return $info;
        }

update()

Update the row in the database corresponding to the current object, using the primary key and its value on the object.

        $book->update(
                {
                        name => 'Learning Perl',
                }
        );

This method supports the following optional arguments:

  • skip_modified_update (default 0)

    Do not update the 'modified' field. This is useful if you're using 'modified' to record when was the last time a human changed the row, but you want to exclude automated changes.

  • dbh

    A different database handle than the default one specified in static_class_info(), but it has to be writable.

  • restrictions

    The update statement is limited using the primary key. This parameter however allows adding extra restrictions on the update. Additional clauses passed here are joined with AND.

            $book->update(
                    {
                            author_id => 1234,
                    },
                    restrictions =>
                    {
                            where_clauses => [ 'status != ?' ],
                            where_values  => [ 'protected' ],
                    },
            );
  • set

    \%data contains the data to update the row with "SET field = value". It is however sometimes necessary to use more complex SETs, such as "SET field = field + value", which is what this parameter allows.

    Important: you will need to subclass update() in your model classes and update manually the values upon success (or reload the object), as DBIx::NinjaORM cannot determine the end result of those complex sets on the database side.

            $book->update(
                    {
                            name => 'Learning Perl',
                    },
                    set =>
                    {
                            placeholders => [ 'edits = edits + ?' ],
                            values       => [ 1 ],
                    }
            );

validate_data()

Validate the hashref of data passed as first argument. This is used both by insert() and update to check the data before performing databse operations.

        my $validated_data = $object->validate_data(
                \%data,
        );

If there is invalid data, the method will croak with a detail of the error.

UTILITY METHODS

dump()

Return a string representation of the current object.

        my $string = $book->dump();

flatten_object()

Return a hash with the requested key/value pairs based on the list of fields provided.

Note that non-native fields (starting with an underscore) are not allowed. It also protects sensitive fields.

#TODO: allow defining sensitive fields.

        my $book_data = $book->flatten_object(
                [ 'name', 'isbn' ]
        );

reload()

Reload the content of the current object. This always skips the cache.

        $book->reload();

retrieve_list()

Return an arrayref of objects matching all the criteria passed.

This method supports the following filtering criteria in a hashref passed as first argument:

  • id

    An ID or an arrayref of IDs corresponding to the primary key.

            # Retrieve books with ID 1.
            my $books = My::Model::Book->retrieve_list(
                    {
                            id => 1,
                    }
            );
    
            # Retrieve books with IDs 1, 2 or 3.
            my $books = My::Model::Book->retrieve_list(
                    {
                            id => [ 1, 2, 3 ]
                    }
            );
  • Field names

    A scalar value or an arrayref of values corresponding to a field listed in static_class_info() under either filtering_fields or unique_fields.

            # Retrieve books for an author.
            my $books = My::Model::Book->retrieve_list(
                    {
                            author_id => 12,
                    }
            );
    
            # Retrieve books by ISBN.
            my $books = My::Model::Book->retrieve_list(
                    {
                            isbn =>
                            [
                                    '9781449313142',
                                    '9781449393090',
                            ]
                    }
            );

Note that you can combine filters (which is the equivalent of AND in SQL) in that hashref:

        # Retrieve books by ISBN for a specific author.
        my $books = My::Model::Book->retrieve_list(
                {
                        isbn      =>
                        [
                                '9781449313142',
                                '9781449393090',
                        ],
                        author_id => 12,
                }
        );

Filters as discussed above, imply an equality between the field and the values. For instance, in the last example, the request could be written as "Please provide a list of books with author_id equal to 12, which also have an ISBN equal to 9781449313142 or an ISBN equal to 9781449393090".

If you wish to request records using some other operator than equals, you can create a request similar to the following:

        # Retrieve books for a specific author with ISBNs starting with a certain pattern.
        my $books = My::Model::Book->retrieve_list(
                {
                        isbn      =>
                        {
                                operator => 'like',
                                value => [ '9781%' ],
                        },
                        author_id => 12,
                }
        );

The above example could be written as "Please provide a list of books with author_id equal to 12, which also have an ISBN starting with 9781".

Valid operators include:

        * =
        * not
        * <=
        * >=
        * <
        * >
        * between
        * null
        * not_null
        * like
        * not_like

This method also supports the following optional arguments, passed in a hash after the filtering criteria above-mentioned:

  • dbh

    Retrieve the data against a different database than the default one specified in static_class_info.

  • order_by

    Specify an ORDER BY clause to sort the objects returned.

            my $books = My::Model::Book->retrieve_list(
                    {
                            author_id => 12,
                    },
                    order_by => 'books.name ASC',
            );
  • limit

    Limit the number of objects to return.

            # Get 10 books from author #12.
            my $books = My::Model::Book->retrieve_list(
                    {
                            author_id => 12,
                    },
                    limit => 10,
            );
  • query_extensions

    Add joins and support different filtering criteria:

    • where_clauses

      An arrayref of clauses to add to WHERE.

    • where_values

      An arrayref of values corresponding to the clauses.

    • joins

      A string specifying JOIN statements.

    • joined_fields

      A string of extra fields to add to the SELECT.

            my $books = My::Model::Book->retrieve_list(
                    {
                            id => [ 1, 2, 3 ],
                    },
                    query_extensions =>
                    {
                            where_clauses => [ 'authors.name = ?' ],
                            where_values  => [ [ 'Randal L. Schwartz' ] ],
                            joins         => 'INNER JOIN authors USING (author_id)',
                            joined_fields => 'authors.name AS _author_name',
                    }
            );
  • pagination

    Off by default. Paginate the results. You can control the pagination options by setting this to the following hash, with each key being optional and falling back to the default if you omit it:

            my $books = My::Model::Book->retrieve_list(
                    {},
                    allow_all  => 1,
                    pagination =>
                    {
                            # The number of results to retrieve.
                            per_page    => $per_page,
                            # Number of the page of results to retrieve. If you have per_page=10
                            # and page=2, then this would retrieve rows 10-19 from the set of
                            # matching rows.
                            page        => $page,
                    }
            );

    Additionally, pagination can be set to '1' instead of {} and then the default options will be used.

    More pagination information is then returned in list context:

            my ( $books, $pagination ) = My::Model::Book->retrieve_list( ... );

    With the following pagination information inside $pagination:

            {
                    # The total number of rows matching the query.
                    total_count => $total_count,
                    # The current page being returned.
                    page        => $page,
                    # The total number of pages to display the matching rows.
                    page_max    => $page_max,
                    # The number of rows displayed per page.
                    per_page    => $per_page,
            }
  • lock (default 0)

    Add a lock to the rows retrieved.

            my $books = My::Model::Book->retrieve_list(
                    {
                            id => [ 1, 2, 3 ],
                    },
                    lock => 1,
            );
  • allow_all (default 0)

    Retrieve all the rows in the table if no criteria is passed. Off by default to prevent retrieving large tables at once.

            # All the books!
            my $books = My::Model::Book->retrieve_list(
                    {},
                    allow_all => 1,
            );
  • show_queries (default 0)

    Set to '1' to see in the logs the queries being performed.

            my $books = My::Model::Book->retrieve_list(
                    {
                            id => [ 1, 2, 3 ],
                    },
                    show_queries => 1,
            );
  • allow_subclassing (default 0)

    By default, retrieve_list() cannot be subclassed to prevent accidental infinite recursions and breaking the cache features provided by NinjaORM. Typically, if you want to add functionality to how retrieving a group of objects works, you will want to modify retrieve_list_nocache() instead.

    If you really need to subclass retrieve_list(), you will then need to set allow_subclassing to 1 in subclassed method's call to its parent, to indicate that you've carefully considered the impact of this and that it is safe.

  • select_fields / exclude_fields (optional)

    By default, retrieve_list() will select all the fields that exist on the table associated with the class. In some rare cases, it is however desirable to either select only or to exclude explicitely some fields from the table, and you can pass an arrayref with select_fields and exclude_fields (respectively) to specify those.

    Important cache consideration: when this option is used, the cache will be used to retrieve objects without polling the database when possible, but any objects retrieved from the database will not be stashed in the cache as they will not have the complete information for that object. If you have other retrieve_list() calls warming the cache this most likely won't be an issue, but if you exclusively run retrieve_list() calls with select_fields and exclude_fields, then you may be better off creating a view and tieing the class to that view.

            # To display an index of our library, we want all the book properties but not
            # the book content, which is a huge field that we won't use in the template.
            my $books = My::Model::Book->retrieve_list(
                    {},
                    allow_all => 1,
                    exclude_fields => [ 'full_text' ],
            );

ACCESSORS

get_cache_key_field()

Return the name of the field that should be used in the cache key.

        my $cache_time = $class->cache_key_field();
        my $cache_time = $object->cache_key_field();

get_default_dbh()

WARNING: this method will be removed soon. Use get_info('default_dbh') instead.

Return the default database handle to use with this class.

        my $default_dbh = $class->get_default_dbh();
        my $default_dbh = $object->get_default_dbh();

get_filtering_fields()

Returns the fields that can be used as filtering criteria in retrieve_list().

Notes:

  • Does not include the primary key.

  • Includes unique fields.

            my $filtering_fields = $class->get_filtering_fields();
            my $filtering_fields = $object->get_filtering_fields();

get_info()

Return cached static class information for the current object or class.

        my $info = $class->get_info();
        my $info = $object->get_info();

get_list_cache_time()

WARNING: this method will be removed soon. Use get_info('list_cache_time') instead.

Return the duration for which a list of objects of the current class can be cached.

        my $list_cache_time = $class->list_cache_time();
        my $list_cache_time = $object->list_cache_time();

get_memcache()

WARNING: this method will be removed soon. Use get_info('memcache') instead.

Return the memcache object to use with this class.

        my $memcache = $class->get_memcache();
        my $memcache = $object->get_memcache();

get_object_cache_time()

WARNING: this method will be removed soon. Use get_info('object_cache_time') instead.

Return the duration for which an object of the current class can be cached.

        my $object_cache_time = $class->get_object_cache_time();
        my $object_cache_time = $object->get_object_cache_time();

get_primary_key_name()

WARNING: this method will be removed soon. Use get_info('primary_key_name') instead.

Return the underlying primary key name for the current class or object.

        my $primary_key_name = $class->get_primary_key_name();
        my $primary_key_name = $object->get_primary_key_name();

get_readonly_fields()

WARNING: this method will be removed soon. Use get_info('readonly_fields') instead.

Return an arrayref of fields that cannot be modified via set(), update(), or insert().

        my $readonly_fields = $class->get_readonly_fields();
        my $readonly_fields = $object->get_readonly_fields();

get_table_name()

WARNING: this method will be removed soon. Use get_info('table_name') instead.

Returns the underlying table name for the current class or object.

        my $table_name = $class->get_table_name();
        my $table_name = $object->get_table_name();

get_unique_fields()

WARNING: this method will be removed soon. Use get_info('unique_fields') instead.

Return an arrayref of fields that are unique for the underlying table.

Important: this doesn't include the primary key name. To retrieve the name of the primary key, use $class-primary_key_name()>

        my $unique_fields = $class->get_unique_fields();
        my $unique_fields = $object->get_unique_fields();

has_created_field()

WARNING: this method will be removed soon. Use get_info('has_created_field') instead.

Return a boolean to indicate whether the underlying table has a 'created' field.

        my $has_created_field = $class->has_created_field();
        my $has_created_field = $object->has_created_field();

has_modified_field()

WARNING: this method will be removed soon. Use get_info('has_modified_field') instead.

Return a boolean to indicate whether the underlying table has a 'modified' field.

        my $has_modified_field = $class->has_modified_field();
        my $has_modified_field = $object->has_modified_field();

id()

Return the value associated with the primary key for the current object.

        my $id = $object->id();

is_verbose()

Return if verbosity is enabled.

This method supports two types of verbosity:

  • general verbosity

    Called with no argument, this returns whether code in general will be verbose.

            $log->debug( 'This is verbose' )
                    if $class->is_verbose();
            $log->debug( 'This is verbose' )
                    if $object->is_verbose();
  • verbosity for a specific type of operations

    Called with a specific type of operations as first argument, this returns whether that type of operations will be verbose.

            $log->debug( 'Describe cache operation' )
                    if $class->is_verbose( $operation_type );
            $log->debug( 'Describe cache operation' )
                    if $object->is_verbose( $operation_type );

    Currently, the following types of operations are supported:

    • 'cache_operations'

CACHE RELATED METHODS

cached_static_class_info()

Return a cached version of the information retrieved by static_class_info().

        my $static_class_info = $class->cached_static_class_info();
        my $static_class_info = $object->cached_static_class_info();

get_table_schema()

Return the schema corresponding to the underlying table.

        my $table_schema = $class->get_table_schema();
        my $table_schema = $object->get_table_schema();

delete_cache()

Delete a key from the cache.

        my $value = $class->delete_cache( key => $key );

get_cache()

Get a value from the cache.

        my $value = $class->get_cache( key => $key );

get_object_cache_key()

Return the name of the cache key for an object or a class, given a field name on which a unique constraint exists and the corresponding value.

        my $cache_key = $object->get_object_cache_key();
        my $cache_key = $class->get_object_cache_key(
                unique_field => $unique_field,
                value        => $value,
        );

invalidate_cached_object()

Invalidate the cached copies of the current object across all the unique keys this object can be referenced with.

        $object->invalidate_cached_object();

retrieve_list_cache()

Dispatch of retrieve_list() when objects should be retrieved from the cache.

See retrieve_list() for the parameters this method accepts.

set_cache()

Set a value into the cache.

        $class->set_cache(
                key         => $key,
                value       => $value,
                expire_time => $expire_time,
        );

INTERNAL METHODS

Those methods are used internally by DBIx::NinjaORM, you should not subclass them.

assert_dbh()

Assert that there is a database handle, either a specific one passed as first argument to this function (if defined) or the default one specified via static_class_info(), and return it.

        my $dbh = $class->assert_dbh();
        my $dbh = $object->assert_dbh();

        my $dbh = $class->assert_dbh( $custom_dbh );
        my $dbh = $object->assert_dbh( $custom_dbh );

Note that this method also supports coderefs that return a DBI::db object when evaluated. That way, if no database connection is needed when running the code, no connection needs to be established.

build_filtering_clause()

Create a filtering clause using the field, operator and values passed.

        my ( $clause, $clause_values ) = $class->build_filtering_clause(
                field    => $field,
                operator => $operator,
                values   => $values,
        );

parse_filtering_criteria()

Helper function that takes a list of fields and converts them into where clauses and values that can be used by retrieve_list().

        my ( $where_clauses, $where_values, $filtering_field_keys_passed ) =
                @{
                        $class->parse_filtering_criteria(
                                \%filtering_criteria
                        )
                };

$filtering_field_keys_passed indicates whether %values had keys matching at least one element of @field. This allows detecting whether any filtering criteria was passed, even if the filtering criteria do not result in WHERE clauses being returned.

reorganize_non_native_fields()

When we retrieve fields via SELECT in retrieve_list_nocache(), by convention we use _[table_name]_[field_name] for fields that are not native to the underlying table that the object represents.

This method moves them to $object->{'_table_name'}->{'field_name'} for a cleaner organization inside the object.

        $object->reorganize_non_native_fields();

BUGS

Please report any bugs or feature requests through the web interface at https://github.com/guillaumeaubert/DBIx-NinjaORM/issues/new. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

        perldoc DBIx::NinjaORM

You can also look for information at:

AUTHOR

Guillaume Aubert, <aubertg at cpan.org>.

CONTRIBUTORS

ACKNOWLEDGEMENTS

I originally developed this project for ThinkGeek (http://www.thinkgeek.com/). Thanks for allowing me to open-source it!

Special thanks to Kate Kirby for her help with the design of this module.

COPYRIGHT & LICENSE

Copyright 2009-2017 Guillaume Aubert.

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

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for more details.