—package
WWW::SFDC::Manifest;
# ABSTRACT: Utility functions for Salesforce Metadata API interactions.
use
5.12.0;
use
strict;
use
warnings;
our
$VERSION
=
'0.36'
;
# TRIAL VERSION
use
Data::Dumper;
use
Method::Signatures;
use
XML::Parser;
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"
;
$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