I mentioned at the Ephemeral Miniconf last month that as soon as I write about one Perl module (or five), someone inevitably brings up another (or seven) I’ve missed. And of course, it happened again last week: no sooner had I written in passing that I was using Exception::Class than the denizens of the Libera Chat IRC#perl channel insisted I should use Throwable instead for defining my exceptions. (I’ve already blogged about various ways of catching exceptions.)
Why Throwable? Aside from Exception::Class’s author recommending it over his own work due to a “nicer, more modern interface,” Throwable is a Moo role, so it’s composable into classes along with other roles instead of mucking about with multiple inheritance. This means that if your exceptions need to do something reusable in your application like logging, you can also consume a role that does that and not have so much duplicate code. (No, I’m not going to pick a favorite logging module; I’ll probably get that wrong too.)
However, since Throwable is a role instead of a class, I would have to define several additional packages in my tiny modulino script from last week, one for each exception class I want. The beauty of Exception::Class is its simple declarative nature: just use it and pass a list of desired class names along with options for attributes and whatnot. What’s needed for simple use cases like mine is a declarative syntax for defining several exception classes without the noise of multiple packages.
Enter Throwable::SugarFactory, a module that enables you to do just that by adding an exception function for declaring exception classes. (There’s also the similarly-named Throwable::Factory; see the above discussion about never being able to cover everybody’s favorites.) The exception function takes three arguments: the name of the desired exception class as a string, a description, and an optional list of instructions Moo uses to build the class. It might look something like this:
package Local::My::Exceptions;
use Throwable::SugarFactory;
exception GenericError => 'something bad happened';
exception DetailedError => 'something specific happened' =>
( has => [ message => ( is => 'ro' ) ] );
1;
Throwable::SugarFactory takes care of creating constructor functions in Perl-style snake_case as well as functions for detecting what kind of exception is being caught, so you can use your new exception library like this:
#!/usr/bin/env perl
use experimental qw(isa);
use Feature::Compat::Try;
use JSON::MaybeXS;
use Local::My::Exceptions;
try {
die generic_error();
}
catch ($e) {
warn 'whoops!';
}
try {
die detailed_error( message => 'you got me' );
}
catch ($e) {
die encode_json( $e->to_hash )
if $e isa DetailedError and defined $e->message;
$e->throw if $e->does('Throwable');
die $e;
}
The above also demonstrates a couple of other Throwable::SugarFactory features. First, you get a to_hash method that returns a hash reference of all exception data, suitable for serializing to JSON. Second, you get all of Throwable’s methods, including throw for re-throwing exceptions.
So where does this leave last week’s FOAAS.com modulino client demonstration of object mocking tests? With a little bit of rewriting to define and then use our sweeter exception library, it looks like this. You can review for a description of the rest of its workings.
#!/usr/bin/env perl
package Local::CallFOAAS::Exceptions;
use Throwable::SugarFactory;
BEGIN {
exception NoMethodError =>
'no matching WebService::FOAAS method' =>
( has => [ method => ( is => 'ro' ) ] );
exception ServiceError =>
'error from WebService::FOAAS' =>
( has => [ message => ( is => 'ro' ) ] );
}
package Local::CallFOAAS; # this is a modulino
use Test2::V0; # enables strict, warnings, utf8
# declare all the new stuff we're using
use feature qw(say state);
use experimental qw(isa postderef signatures);
use Feature::Compat::Try;
use Syntax::Construct qw(non-destructive-substitution);
use WebService::FOAAS ();
use Package::Stash;
BEGIN { Local::CallFOAAS::Exceptions->import() }
my $foaas = Package::Stash->new('WebService::FOAAS');
my $run_as =
!!$ENV{CPANTEST} ? 'test'
: !defined scalar caller ? 'run'
: undef;
__PACKAGE__->$run_as(@ARGV) if defined $run_as;
sub run ( $class, @args ) {
try { say $class->call_method(@args) }
catch ($e) {
die 'No method ', $e->method, "\n"
if $e isa NoMethodError;
die 'Service error: ', $e->message, "\n"
if $e isa ServiceError;
die "$e\n";
}
return;
}
# Utilities
sub methods ($) {
state @methods = sort map s/^foaas_(.+)/$1/r,
grep /^foaas_/, $foaas->list_all_symbols('CODE');
return @methods;
}
sub call_method ( $class, $method = '', @args ) {
state %methods = map { $_ => 1 } $class->methods();
die no_method_error( method => $method )
unless $methods{$method};
return do {
try { $foaas->get_symbol("&$method")->(@args) }
catch ($e) { die service_error( message => $e ) }
};
}
# Testing
sub test ( $class, @ ) {
state $stash = Package::Stash->new($class);
state @tests = sort grep /^_test_/,
$stash->list_all_symbols('CODE');
for my $test (@tests) {
subtest $test => sub {
try { $class->$test() }
catch ($e) { diag $e }
};
}
done_testing();
return;
}
sub _test_can ($class) {
state @subs = qw(run call_method methods test);
can_ok $class, \@subs, "can do: @subs";
return;
}
sub _test_methods ($class) {
my $mock = mock 'WebService::FOAAS' => ( track => 1 );
for my $method ( $class->methods() ) {
$mock->override( $method => 1 );
ok lives { $class->call_method($method) },
"$method lives";
ok scalar $mock->sub_tracking->{$method}->@*,
"$method called";
}
return;
}
sub _test_service_failure ($class) {
my $mock = mock 'WebService::FOAAS';
for my $method ( $class->methods() ) {
$mock->override( $method => sub { die 'mocked' } );
my $exception =
dies { $class->call_method($method) };
isa_ok $exception, [ServiceError],
"$method throws ServiceError on failure";
like $exception->message, qr/^mocked/,
"correct error in $method exception";
}
return;
}
1;
[Updated, thanks to Dan Book, Karen Etheridge, and Bob Kleemann] The only goofy bit above is the need to put the exception calls in a BEGIN block and then explicitly call BEGIN { Local::CallFOAAS::Exceptions->import() }. Since the two packages are in the same file, I can’t do a use statement since the implied require would look for a corresponding file or entry in %INC. (You can get around this by messing with %INC directly or through a module like me::inlined that does that messing for you, but for a single-purpose modulino like this it’s fine.)
Let’s assume for the moment that you’re writing a Perl module or application. You’d like to maintain some level of software quality (or kwalitee), so you’re writing a suite of test scripts. Whether you’re writing them first (good for you for practicing test-driven development!) or the application code is already there, you’ll probably be reaching for Test::Simple, Test::More, or one of the Test2::Suite bundles. With the latter two you’re immediately confronted with a choice: do you count up the number of tests into a plan, or do you forsake that in favor of leaving a done_testing() call at the end of your test script(s)?
There are good arguments for both approaches. When you first start, you probably have no idea how many tests your scripts will contain. After all, a test script can be a useful tool for designing a module’s interface by writing example code that will use it. Your exploratory code would be written as if the module or application was already done, testing it in the way you’d like it to work. Not declaring a plan makes perfect sense in this case; just put done_testing() at the end and get back to defining your tests.
You don’t have that option when using Test::Simple, of course—it’s so basic it only has one function (ok()), and you have to pre-declare how many tests you plan to run when useing the module, like so:
use Test::Simple tests => 23;
Test::More also supports this form of plan, or you can opt to use its plan function to state the number of tests in your script or subtest. With Test2 you have to use plan. Either way, the plan acts as a sort of meta-test, making sure that you executed exactly what you intended: no more, no less. While there are situations where it’s not possible to predict how many times a given set of tests should run, I would highly suggest that in all other cases you should “clean up” your tests and declare a plan. Later on, if you add or remove tests you’ll immediately be aware that something has changed and it’s time to tally up a new plan.
What about other Perl testing frameworks? They can use plans, too. Here are two examples:
Thoughts? Does declaring a test plan make writing tests too inflexible? Does not having a plan encourage bad behavior? Tell me what you think in the comments below.
All examples below use the bash or zsh command shells and were tested on macOS Catalina 10.15.7 running zsh5.7.1 and Perl 5.32.1. If you’re using something very different (e.g., Microsoft Windows’ CMD or PowerShell), you may have to set environment variables differently.
Ad-hoc test coverage
If all you want to do is run one shell command, here it is:
$ prove -vlre 'perl -MDevel::Cover -Ilib' t
This takes advantage of prove’s --exec option (abbreviated as -e) to run a different executable for tests. It recursively (-r) runs all your tests verbosely (-v) from the t directory while loading your application’s libraries (-l), while the perl executable uses (-M) Devel::Cover and the lib subdirectory. I use a similar technique when debugging tests.
$ HARNESS_PERL_SWITCHES=-MDevel::Cover prove -vlr t
This does almost the same thing as above without running a different executable. It sets Test::Harness’ HARNESS_PERL_SWITCHES environment variable for the duration of the prove command. You won’t get the text output of your test coverage at the end, though, and will still have to run Devel::Cover’s cover command to both see the coverage and generate web pages.
In a dedicated test session, window, or tab
If you have a terminal session, window, or tab dedicated solely to running your tests, set one of the environment variables above for that session:
$ export HARNESS_PERL_SWITCHES=-MDevel::Cover
Now all of your test scripts will pick up that option. You can add more options by enclosing the environment variable’s value in 'quotes'. For example, you might also want to load Devel::NYTProf for code profiling:
Setting the PERL5OPT environment variable also sets options for the perl running prove, which means that your test coverage, profiling, etc. will pick up prove’s execution as well as your test scripts.
I don’t know for sure; I don’t use the Test2 suite. But it looks like it has a --cover option for loading and passing option to Devel::Cover.
{"id":"3","mode":"text_link","open_style":"in_place","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/phoenixtrap.com\/wp-content\/uploads\/2021\/02\/image-200x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Do you like what you see? Leave a tip!","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"The Phoenix Trap","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"The Phoenix Trap","form_subtitle":"Do you like what you see? Leave a one-time or recurring tip!","currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"The Phoenix Trap","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
{"id":"11","mode":"button","open_style":"in_modal","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/phoenixtrap.com\/wp-content\/uploads\/2021\/02\/image-200x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Leave a tip!","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"The Phoenix Trap","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"The Phoenix Trap","form_subtitle":"Do you like what you see? Leave a one-time or recurring tip!","currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"The Phoenix Trap","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"What is the name on your credit card?"},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"What is your email address?"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}