Coercing ArrayRef[MyClass] from ArrayRef[HashRef]

2019-04-11 19:56发布

In trying to answer How to instantiate Moose classes from a big hash, I think I have hit another place where I don't fully understand Moose type coercions. For some reason, the below code issues warnings:

You cannot coerce an attribute (departments) unless its type (ArrayRef[Company::Department]) has a coercion at ./test.pl line 12.
You cannot coerce an attribute (employees) unless its type (ArrayRef[Company::Person]) has a coercion at ./test.pl line 23.

but then succeeds.

#!/usr/bin/env perl

use warnings;
use strict;

package Company;
use Moose;
use Moose::Util::TypeConstraints;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]', coerce => 1);

coerce 'ArrayRef[Company::Department]',
  from 'ArrayRef[HashRef]',
  via { [ map { Company::Department->new($_) } @$_ ] };

package Company::Department;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]', coerce => 1);

package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;

has 'id'         => (is => 'ro', isa => 'Num');
has 'name'  => (is => 'ro', isa => 'Str');
has 'age'        => (is => 'ro', isa => 'Num');

coerce 'ArrayRef[Company::Person]',
  from 'ArrayRef[HashRef]',
  via { [ map { Company::Person->new($_) } @$_ ] };

package main;

my %hash = (
    company => {
        id => 1,
        name => 'CorpInc',
        departments => [
            {
                id => 1,
                name => 'Sales',
                employees => [
                    {
                        id => 1,
                        name => 'John Smith',
                        age => '30',
                    },
                ],
            },
            {
                id => 2,
                name => 'IT',
                employees => [
                    {
                        id => 2,
                        name => 'Lucy Jones',
                        age => '28',
                    },
                    {
                        id => 3,
                        name => 'Miguel Cerveza',
                        age => '25',
                    },
                ],
            },
        ],
    }
);

my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;

How should this have been done? P.S. I tried simply doing

coerce 'Company::Department',
  from 'HashRef',
  via { Company::Department->new($_) };

but it died horribly.

2条回答
唯我独甜
2楼-- · 2019-04-11 20:42

Well, it doesn't succeed completely, and you should feel it when you'll try to update these fields with coerce => 1. That's why:

You cannot pass coerce => 1 unless the attribute's type constraint has a coercion

Previously, this was accepted, and it sort of worked, except that if you attempted to set the attribute after the object was created, you would get a runtime error. Now you will get an error when you attempt to define the attribute.

Still, I think I find the way to fix it, by introducing subtypes, first, and changing the order of packages, second:

package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;

subtype 'ArrayRefCompanyPersons',
  as 'ArrayRef[Company::Person]';

coerce 'ArrayRefCompanyPersons',
  from 'ArrayRef[HashRef]',
  via { [ map { Company::Person->new($_) } @$_ ] };

has 'id'         => (is => 'ro', isa => 'Num');
has 'name'  => (is => 'ro', isa => 'Str');
has 'age'        => (is => 'ro', isa => 'Num');

package Company::Department;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRefCompanyPersons', coerce => 1);

package Company;
use Moose;
use Moose::Util::TypeConstraints;

subtype 'ArrayRefCompanyDepartments',
  as 'ArrayRef[Company::Department]';

coerce 'ArrayRefCompanyDepartments',
  from 'ArrayRef[HashRef]',
  via { [ map { Company::Department->new($_) } @$_ ] };

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRefCompanyDepartments', coerce => 1);

The rest of the code is the same as in your version. This works without any warnings, and more-o-less behaves like (again, I think) it should be.

查看更多
老娘就宠你
3楼-- · 2019-04-11 20:56

From Moose::Manual::Type docs:

LOAD ORDER ISSUES

Because Moose types are defined at runtime, you may run into load order problems. In particular, you may want to use a class's type constraint before that type has been defined.

In order to ameliorate this problem, we recommend defining all of your custom types in one module, MyApp::Types, and then loading this module in all of your other modules.


So to add to raina77ow subtype & package order answer (+1) I would recommend creating a Company::Types module:

package Company::Types;
use Moose;
use Moose::Util::TypeConstraints;

subtype 'CompanyDepartments'
  => as 'ArrayRef[Company::Department]';

subtype 'CompanyPersons'
  => as 'ArrayRef[Company::Person]';

coerce 'CompanyDepartments'
  => from 'ArrayRef[HashRef]'
  => via { 
       require Company::Department; 
       [ map { Company::Department->new($_) } @$_ ]; 
     };

coerce 'CompanyPersons'
  => from 'ArrayRef[HashRef]'
  => via { require Company::Person; [ map { Company::Person->new($_) } @$_ ] };

1;

And then put use Company::Types in all your Company:: classes.

查看更多
登录 后发表回答