NAME
Curio - Procurer of fine resources and services.
SYNOPSIS
Create a Curio class:
package MyApp::Service::Cache;
use CHI;
use Types::Standard qw( InstanceOf HashRef );
use Type::Utils -all;
use Curio;
use strictures 2;
does_caching;
cache_per_process;
export_function_name 'myapp_cache';
always_export;
export_resource;
resource_method_name 'chi';
add_key geo_ip => (
chi => {
driver => 'Memory',
global => 0,
},
);
my $chi_type = declare as InstanceOf[ 'CHI::Driver' ];
coerce $chi_type, from HashRef, via { CHI->new( %$_ ) };
has chi => (
is => 'ro',
isa => $chi_type,
required => 1,
coerce => 1,
);
1;
Then use your new Curio class elsewhere:
use MyApp::Service::Cache;
my $chi = myapp_cache('geo_ip');
DESCRIPTION
Curio is a toolbox for building a class which holds a resource (or many resources) of your making. Then, in your applications, you can access the resource(s) from anywhere.
INTRODUCTION
Curio is a library for creating Moo classes which encapsulate the construction and retrieval of arbitrary resources. As a user of this library you've got two jobs.
First, you create classes in your application which use Curio. You'll have one class for each type of resource you want available to your application as a whole. So, for example, you'd have a Curio class for your database connections, another for your graphite client, and perhaps a third for your CRM client.
Your second job is to then modify your application to use your Curio classes. If your application uses an existing framework, such as Catalyst, then you may want to take a look at the available "INTEGRATIONS".
Keep in mind that Curio doesn't just have to be used for connections to remote services. It can be used to make singleton classes, as a ready to go generic object factory, a place to put global application context information, etc.
MOTIVATION
The main drive behind creating Curio is threefold.
To avoid the extra complexity of passing around references of shared resources, such as connections to services. Often times you'll see code which passes a connection to a function, which then passes that on to another function, which then creates an object with the connection passed as an argument, etc. This is what is being avoided; it's a messy way to write code and prone to error.
To have a central place to put object creation logic. When there is no central place to put this sort of logic it tends to be haphazardly copy-pasted and sprinkled all over a codebase making it difficult to find and change.
To not be tied into any single framework as is commonly done today. There is no reason this sort of logic needs to be framework dependent, and once it is it makes all sorts of things more difficult, such as migrating frameworks and writing in-house libraries that are framework independent. Yes, Curio is a sort framework itself, but it is a very slim framework which gets out of your way quickly and is designed for this one purpose.
These challenges can be solved, and using Curio can support you in doing so.
IMPORT ARGUMENTS
role
use Curio role => '::CHI';
use Curio role => 'Curio::Role::CHI';
Set this to change the role that is applied to your Curio class.
If the role you specify has a leading ::
it is assumed to be relative to the Curio::Role
namespace and will have that appended to it. So, if you set the role to ::CHI
it will be automatically converted to Curio::Role::CHI
.
See "ROLES" for a list of existing Curio roles.
The default role is Curio::Role.
BOILERPLATE
Near the top of most Curio classes is this line:
use Curio;
Which is exactly the same as:
use Moo;
use Curio::Declare;
use namespace::clean;
BEGIN { with 'Curio::Role' }
BEGIN { __PACKAGE__->initialize() }
If you're not into the declarative interface, or have some other reason to switch around this boilerplate, you may copy the above and modify to fit your needs rather than using this module directly.
Read more about Moo and namespace::clean if you are not familiar with them.
TOPICS
Exporting the Fetch Function
To get at a Curio object's resource takes a lot of typing by default.
my $chi = MyApp::Service::Cache->fetch( $key )->chi();
Creating an export function that wraps this all up is a great way to simplify things. In your Curio class you can set "export_function_name" in Curio::Factory which will create a function, create the @EXPORT_OK
package variable, and add the new function to it.
# In your Curio class.
export_function_name 'myapp_cache';
# Elsewhere.
use MyApp::Service::Cache qw( myapp_cache );
my $chi = myapp_cache()->chi();
If you'd like the function to be always exported (use @EXPORT
) then set "always_export" in Curio::Factory.
# In your Curio class.
export_function_name 'myapp_cache';
always_export;
# Elsewhere.
use MyApp::Service::Cache;
my $chi = myapp_cache()->chi();
If you'd like the exported function to return the resource object instead of the curio object set "export_resource" in Curio::Factory.
# In your Curio class.
export_function_name 'myapp_cache';
export_resource;
resource_method_name 'chi';
# Elsewhere.
use MyApp::Service::Cache qw( myapp_cache );
my $chi = myapp_cache();
The generated function can be overriden with your own custom function.
# In your Curio class.
export_function_name 'myapp_cache';
sub myapp_cache {
return __PACKAGE__->factory->fetch_curio( @_ )->chi();
}
Want to do something more advanced? Don't use any of the methods suggested here and just use the exporter module of your choice.
Caching
Caching is enabled with "does_caching" in Curio::Factory.
does_caching;
When enabled, all curio objects will be cached so that future fetches for a curio object will return the same one as before. This option should almost always be set as it usually provides a huge performance increase.
"cache_per_process" in Curio::Factory extends the caching to handle process/thread changes gracefully.
cache_per_process;
Keys
Curio supports fetching curio objects by key. This is an optional feature and by default is turned off. To turn it on you set "does_keys" in Curio::Factory or just start adding keys with "add_key" in Curio::Factory which will automatically turn on does_keys
.
When keys are enabled a curio class is able to produce different objects based on the key. For example, lets say you have two databases, you could create two curio classes, or you could just enable keys.
add_key db1 => ( host => 'db1.example.com' );
add_key db2 => ( host => 'db2.example.com' );
When keys are enabled calling fetch requires that you pass a key.
my $dbh1 = MyApp::Service::DB->fetch(
'db1', # <-- key
)->dbh();
Passing a key that has not yet been declared with add_key
will throw an error. This can be changed by setting "allow_undeclared_keys" in Curio::Factory.
allow_undeclared_keys;
You can also set "default_key" in Curio::Factory.
default_key 'db1';
Curio objects, by default, have no way of knowing what key was used to make them. If you need to know what key was used to fetch a curio object you can set "key_argument" in Curio::Factory.
key_argument 'key';
has key => ( is=>'ro' );
The "default_arguments" in Curio::Factory option can be useful when you are not using Moo attributes but still need to set defaults for arguments.
default_arguments ( username => 'dbuser' );
The Registry
The registry is a lookup table holding memory addresses of resource objects pointing at references to curio objects. What this means is, if "does_registry" in Curio::Factory is set, you can use "find_curio" in Curio::Role to retrieve the curio object for a given resource object.
does_registry;
resource_method_name 'chi';
In the "SYNOPSIS" "resource_method_name" in Curio::Factory is set to chi
, which is a Moo attribute. When the curio object is created this resource method will be called to get the resource object and, along with the curio object, register them in the registry.
my $curio = MyApp::Service::Cache->find_curio( $chi );
Setting "installs_curio" in Curio::Factory will install a curio
method in resource object classes.
# In your curio class:
does_registry;
resource_method_name 'chi';
installs_curio;
# Elsewhere:
my $curio = $chi->curio();
Injecting Mock Objects
Use "inject" in Curio::Role to force fetch to return a custom curio object.
my $mock = MyApp::Service::Cache->new(
driver => 'Memory',
global => 0,
);
MyApp::Service::Cache->inject( 'geo_ip', $mock );
my $chi = myapp_cache( 'geo_ip' );
MyApp::Service::Cache->uninject( 'geo_ip' );
Instead of having to call "uninject" in Curio::Role directly you may instead use "inject_with_guard" in Curio::Role.
my $guard = MyApp::Service::Cache->inject_with_guard(
'geo_ip', $mock,
);
When the guard object goes out of scope uninject
will be called automatically.
Singletons
Creating a singleton class is super simple.
package MyApp::Context;
use Types::Common::String qw( SimpleStr );
use Curio;
use strictures 2;
export_function_name 'myapp_context';
always_export;
has install_dir => (
is => 'lazy',
isa => SimpleStr,
);
sub _build_install_dir {
my $dir = $ENV{MYAPP_INSTALL_DIR};
return $dir if $dir;
return '/myapp';
}
has config_file => (
is => 'lazy',
isa => SimpleStr,
);
sub _build_config_file {
my ($self) = @_;
my $file = $ENV{MYAPP_CONFIG_FILE};
return $file if $file;
return $self->install_dir() . '/config.ini';
}
See "Configuration" and "Secrets" for more singleton examples.
Configuration
Application configuration is one of those systems which can benefit a lot from being wrapped up in a curio class.
package MyApp::Config;
use Config::INI::Reader;
use MyApp::Context;
use Types::Common::String qw( SimpleStr );
use Types::Standard qw( HashRef );
use Curio;
use strictures 2;
export_function_name 'myapp_config';
always_export;
export_resource;
resource_method_name 'config';
does_caching;
has file => (
is => 'lazy',
isa => SimpleStr,
);
sub _build_file {
return myapp_context()->config_file();
}
has config => (
is => 'lazy',
isa => HashRef,
);
sub _build_config {
my ($self) = @_;
return Config::INI::Reader->read_file(
$self->file(),
);
}
This curio-based configuration class could then be used from anywhere to access your application's configuration.
use MyApp::Config;
print myapp_config()->{login_rate_limit};
See "Singletons" for a working example of MyApp::Context
.
Secrets
Handling secrets requires careful planning and prior experience to get right. It is very easy to leak secrets into logs, onto HTTP error pages, emails, and other locations. Start off right and you can avoid these issues in a lot of common cases by adhering to one important rule: never store your secrets in objects as plain string values.
Whenever you need a secret, request it from your secret storage. Your secret storage may just be a configuration file initialy, and thats ok when you are just starting, but once your project starts being used by more than other developers and testers you'll need to consider a more robust solution, such as Vault or Kubernetes secret objects.
The important thing is that your API for accessing secrets remain consistant even as you transition from one secret storage to another. Wrapping your secret storage in a curio class can make these sorts of transitions run smoothly as only the code in your curio class needs to be changed.
package MyApp::Secrets;
use MyApp::Service::Vault;
use Curio;
use strictures 2;
export_function_name 'myapp_secret';
always_export;
does_caching;
sub myapp_secret {
my $key = pop;
return myapp_vault()->secret( path=>$key )->data->{password};
}
This generic interface provided by your curio class would not need changing if you completely moved your secret backend to a different system.
use MyApp::Secrets;
my $db_password = myapp_secret('db-main-www');
Handling Arguments
Migrating and Merging Keys
Introspection
Custom Curio Roles
IMPORTANT PRACTICES
Avoid Holding onto Curio Objects and Resources
Curio is designed to make it cheap to retrieve Curio objects and the underlying resources. Take advantage of this. Don't pass around your resource objects or put them in attributes. Instead, when you need them, get the from your Curio classes.
If your Curio class supports keys, then passing around the key that you want particular code to be using, rather than the Curio object or the resource, is a much better way of handling things.
Read more of the reasoning for this in "MOTIVATION" in Curio.
Use Curio Directly
It is tempting to use the "INTEGRATIONS" in Curio such as Catalyst::Model::Curio, and sometimes it is necessary to do so. Most of the time there is no need to add that extra layer of complexity.
Using Catalyst as an example, there are few reasons you can't just use your Curio classes directly from your Catalyst controllers.
At ZipRecruiter, where we have some massive Catalyst applications, we only use Catalyst models in the few cases where other parts of Catalyst demand that models be setup. For the most part we bypass the model system completely and it makes everything much cleaner and easier to deal with.
Appropriate Uses of Key Aliases
Key aliases are meant as a tool for migrating and merging keys. They are meant to be something you temporarily setup as you change your code to use the new keys, and then once done you remove the aliases.
It can be tempting to use key aliases to provide simpler or alternative names for existing keys. The problem with doing this is now you've introduced multiple keys for the same Curio class which in practice causes unnecessary confusion.
ROLES
These roles, available on CPAN, provide a base set of functionality for your Curio classes to wrap around specific resource types.
INTEGRATIONS
The CPAN modules listed here integrate Curio with other things such as web frameworks.
On a related note, take a look at "Use Curio Directly" in Curio.
SEE ALSO
It is hard to find anything out there on CPAN which is similar to Curio.
There is Bread::Board but it has a very different take and solves different problems.
Catalyst has its models, but that doesn't really apply since they are baked into the framework. The idea is similar though.
Someone started something that looks vaguely similar called Trinket (this was one of the names I was considering and found it by accident) but it never got any love since initial release in 2012 and is incomplete.
Since Curio can do singletons, you may want to check out MooX::Singleton and MooseX::Singleton.
SUPPORT
Please submit bugs and feature requests to the Curio GitHub issue tracker:
https://github.com/bluefeet/Curio/issues
ACKNOWLEDGEMENTS
Thanks to ZipRecruiter for encouraging their employees to contribute back to the open source ecosystem. Without their dedication to quality software development this distribution would not exist.
AUTHORS
Aran Clary Deltac <bluefeet@gmail.com>
COPYRIGHT AND LICENSE
Copyright (C) 2019 Aran Clary Deltac
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
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 GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.