Perl x Open Food Facts Hackathon: Paris, France - May 24-25 Learn more

# ABSTRACT: Utility functions for Salesforce Metadata API interactions.
use 5.12.0;
use strict;
our $VERSION = '0.36'; # TRIAL VERSION
use List::Util 1.29 qw'first reduce pairmap pairgrep pairfirst';
use Log::Log4perl ':easy';
use Scalar::Util qw(blessed);
use Moo;
has 'apiVersion', is => 'rw', default => '34';
has 'constants', is => 'ro', required => 1;
has 'isDeletion', is => 'ro';
has 'manifest', is => 'rw', default => sub { {} };
# _splitLine($line)
# Takes a string representing a file on disk, such as "email/foo/bar.email-meta.xml",
# and returns a hash containing the metadata type, folder name, file name, and
# file extension, excluding -meta.xml.
method _splitLine ($line) {
# we're using '' in lots of regexes to avoid escaping / all over the place
# This method could probably be cleaned up by using File::Spec or similar.
$line =~ s".*src/""; # Bin anything up to and including src/.
$line =~ s/[\n\r]//g; # Bin any newline characters.
$line =~ s"\\"/"g; # Turn \ into / for cross-platform paths.
my %result = (extension => "");
($result{type}) = $line =~ m"^(\w+)/" or LOGDIE "Line $line doesn't have a type.";
$result{folder} = $1 if $line =~ m"/(\w+)/";
my $extension = (grep {$_ eq $result{type}} keys $self->constants->TYPES)
? $self->constants->getEnding($result{type})
: undef;
if ($line =~ m"/(\w+)-meta.xml") {
$result{name} = $1
} elsif (!defined $extension) {
($result{name}) = $line =~ m"/([^/]*?)(-meta\.xml)?$";
# This is because components get passed back from listDeletions with : replacing .
$result{name} =~ s":".";
} elsif ($line =~ m"/([^/]*?)($extension)(-meta\.xml)?$") {
$result{name} = $1;
$result{extension} = $2;
}
LOGDIE "Line $line doesn't have a name." unless $result{name};
return \%result;
}
# _getFilesForLine($line)
# Takes a string representing a file on disk, such as "email/foo/bar.email",
# and returns a list representing all the files needed in the zip file for
# that file to be successfully deployed, for example:
# - email/foo-meta.xml
# - email/foo/bar.email
# - email/foo/bar.email-meta.xml
method _getFilesForLine ($line?) {
return () unless $line;
my %split = %{$self->_splitLine($line)};
return map {"$split{type}/$_"} (
$split{"folder"}
? (
"$split{folder}-meta.xml",
"$split{folder}/$split{name}$split{extension}",
(
$self->constants->needsMetaFile($split{"type"})
? "$split{folder}/$split{name}$split{extension}-meta.xml"
: ()
)
)
: (
"$split{name}$split{extension}",
(
$self->constants->needsMetaFile($split{"type"})
? "$split{name}$split{extension}-meta.xml"
: ()
)
)
)
}
# _dedupe($listref)
# Returns a list reference to a _deduped version of the list
# reference passed in.
method _dedupe {
my %result;
for my $key (keys %{$self->manifest}) {
my %_deduped = map {$_ => 1} @{$self->manifest->{$key}};
$result{$key} = [sort keys %_deduped];
}
$self->manifest(\%result);
return $self;
}
method getFileList {
return map {
my $type = $self->constants->getDiskName($_);
my $ending = $self->constants->getEnding($type) || "";
map {
if ($self->constants->hasFolders($type) and $_ !~ /\//) {
"$type/$_-meta.xml";
} else {
"$type/$_$ending", ($self->constants->needsMetaFile($type) ? "$type/$_$ending-meta.xml" : () );
}
} @{ $self->manifest->{$_} }
} keys %{$self->manifest};
}
method add ($new) {
if (defined blessed $new and blessed $new eq blessed $self) {
push @{$self->manifest->{$_}}, @{$new->manifest->{$_}} for keys %{$new->manifest};
} else {
push @{$self->manifest->{$_}}, @{$new->{$_}} for keys %$new;
}
return $self->_dedupe();
}
method addList (@_) {
return reduce {$a->add($b)} $self, map {
TRACE "Adding $_ to manifest";
+{
$self->constants->getName($$_{type}) => [
defined $$_{folder}
? (
($self->isDeletion ? () : $$_{folder}),
"$$_{folder}/$$_{name}"
)
: ($$_{name})
]
}
} map {$self->_splitLine($_)} @_;
}
method readFromFile ($fileName) {
# XML::Parser returns a list which consists of
#
# (
# $attributes,
# $childNodeName, $childNodeElements,
# $childNodeName, $childNodeElements,
# ...
# )
#
# Where each $childNodeElements looks like this too (ie recursive).
#
# We use splice to remove the $attributes which we assume will be empty,
# then use pairmap and pairgrep to find the right nodes to operate on and
# transform the data. Finally, we use reduce (from List::Util) to add each
# resulting hashref to the current manifest.
return reduce {$a->add($b)} $self, map {+{
do {
pairmap {$b->[2]} pairfirst {$a eq 'name'} @$_
} => [
pairmap {$b->[2]} pairgrep {$a eq 'members'} @$_
]
}}
pairmap {[splice @{$b}, 1]} pairgrep {$a eq 'types'}
splice @{
XML::Parser->new(Style=>"Tree")->parsefile($fileName)->[1]
}, 1;
}
method writeToFile ($fileName) {
open my $fh, ">", $fileName or LOGDIE "Couldn't open $fileName to write manifest to disk";
print $fh $self->getXML();
return $self;
}
method getXML {
# Ultra-low-tech.
return join "", (
"<?xml version='1.0' encoding='UTF-8'?>",
(
map {(
"<types>",
"<name>$_</name>",
( map {"<members>$_</members>"} @{$self->manifest->{$_}} ),
"</types>",
)} sort keys %{$self->manifest}
),
"<version>",$self->apiVersion,"</version></Package>"
);
}
1;
__END__
=pod
=head1 NAME
WWW::SFDC::Manifest - Utility functions for Salesforce Metadata API interactions.
=head1 VERSION
version 0.36
=head1 SYNOPSIS
This module is used to read SFDC manifests from disk, add files to them,
and get a structure suitable for passing into WWW::SFDC::Metadata functions.
my $SFDC = WWW::SFDC->new(...);
my $Manifest = WWW::SFDC::Manifest
->new(constants => $SFDC->Constants)
->readFromFile("filename")
->addList()
->add({Document => ["bar/foo.png"]});
my $HashRef = $Manifest->manifest();
my $XMLString = $Manifest->getXML();
=head1 ATTRIBUTES
=head2 apiVersion
This apiVersion will be written to the manifest .xml file - defaults to 34.
Beware that the API version specified in a manifest file overrides the API
version specified in the endpoint URL.
=head2 constants
A L<WWW::SFDC::Constants> object. If you're working with a session, you'll
need to pass in the constants from the session. If you're trying to work
offline, you'll need to create a new Constants object explicitly.
=head2 isDeletions
If set, the manifest will be constructed slightly differently: when you add a
file inside a folder, such as src/email/myfolder/mytemplate.email, normally
the manifest will include both the folder and the template. However, when
deleting, the folder will not be included unless the -meta file is explicitly
specified, i.e. src/email/myfolder-meta.xml. This is because deleting the
folder will delete all members of that folder, and that's not normally the
desired effect.
=head2 manifest
The underlying hash for the Manifest object. This hashref can be passed into a
call to retrieve(), and a WWW::SFDC::Manifest can also be created with this
prepopulated, if you already know what it should contain.
This hashref will look like
{
classes => [
'myclass',
...
],
email => [
'myfolder',
'myfolder/mytemplate',
...
]
...
}
=head1 METHODS
=head2 getFileList(@list)
Returns a list of files needed to deploy this manifest. Use this to construct
a .zip file.
=head2 add($manifest)
Adds an existing manifest object or hash to this one.
=head2 addList($isDeletion, @list)
Adds a list of components or file paths to the manifest file.
=head2 readFromFile $location
Reads a salesforce package manifest and adds it to the current object, then
returns it.
=head2 writeToFile $location
Writes the manifest's XML representation to the given file and returns
the manifest object.
=head2 getXML($mapref)
Returns the XML representation for this manifest.
=head1 BUGS
Please report any bugs or feature requests at L<https://github.com/sophos/WWW-SFDC/issues>.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::SFDC::Manifest
You can also look for information at L<https://github.com/sophos/WWW-SFDC>
=head1 AUTHOR
Alexander Brett <alexander.brett@sophos.com> L<http://alexander-brett.co.uk>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2015 by Sophos Limited L<https://www.sophos.com/>.
This is free software, licensed under:
The MIT (X11) License
The full text of the license can be found in the
F<LICENSE> file included with this distribution.
=cut