initial commit
authorMark Wells <mark@freeside.biz>
Sun, 1 Dec 2013 00:14:46 +0000 (16:14 -0800)
committerMark Wells <mark@freeside.biz>
Sun, 1 Dec 2013 00:14:46 +0000 (16:14 -0800)
.gitignore [new file with mode: 0644]
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
WebSmart.pm [new file with mode: 0644]
ignore.txt [new file with mode: 0644]
t/00-load.t [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9788afa
--- /dev/null
@@ -0,0 +1,6 @@
+blib/
+*.sw?
+Makefile
+Makefile.old
+MYMETA.yml
+pm_to_blib
diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..2083fc0
--- /dev/null
+++ b/Changes
@@ -0,0 +1,3 @@
+Revision history for Geo-Melissa-WebSmart
+
+unreleased
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..8640c27
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,6 @@
+Changes
+WebSmart.pm
+Makefile.PL
+MANIFEST                       This list of files
+README
+t/00-load.t
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..94fd136
--- /dev/null
@@ -0,0 +1,20 @@
+use 5.006;
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+    NAME                => 'Geo::Melissa::WebSmart',
+    AUTHOR              => q{Mark Wells <mark@freeside.biz>},
+    VERSION_FROM        => 'WebSmart.pm',
+    ABSTRACT_FROM       => 'WebSmart.pm',
+    ($ExtUtils::MakeMaker::VERSION >= 6.3002
+      ? ('LICENSE'=> 'perl')
+      : ()),
+    PL_FILES            => {},
+    PREREQ_PM => {
+        'Test::More' => 0,
+    },
+    dist                => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+    clean               => { FILES => 'Geo-Melissa-WebSmart-*' },
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..25e7791
--- /dev/null
+++ b/README
@@ -0,0 +1,46 @@
+Geo-Melissa-WebSmart
+
+Interface to the Melissa Data WebSmart address verification and geocoding
+service.
+
+INSTALLATION
+
+To install this module, run the following commands:
+
+       perl Makefile.PL
+       make
+       make test
+       make install
+
+SUPPORT AND DOCUMENTATION
+
+After installing, you can find documentation for this module with the
+perldoc command.
+
+    perldoc Geo::Melissa::WebSmart
+
+You can also look for information at:
+
+    RT, CPAN's request tracker (report bugs here)
+        http://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Melissa-WebSmart
+
+    AnnoCPAN, Annotated CPAN documentation
+        http://annocpan.org/dist/Geo-Melissa-WebSmart
+
+    CPAN Ratings
+        http://cpanratings.perl.org/d/Geo-Melissa-WebSmart
+
+    Search CPAN
+        http://search.cpan.org/dist/Geo-Melissa-WebSmart/
+
+
+LICENSE AND COPYRIGHT
+
+Copyright (C) 2013 Mark Wells
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
diff --git a/WebSmart.pm b/WebSmart.pm
new file mode 100644 (file)
index 0000000..c54ee19
--- /dev/null
@@ -0,0 +1,328 @@
+package Geo::Melissa::WebSmart;
+
+use strict;
+use warnings;
+
+use LWP::UserAgent;
+use XML::LibXML;
+use URI;
+
+=head1 NAME
+
+Geo::Melissa::WebSmart - The Melissa Data WebSmart address verification service
+
+=head1 VERSION
+
+Version 0.01
+
+=cut
+
+our $VERSION = '0.01';
+our $DEBUG = 0;
+
+=head1 SYNOPSIS
+
+    use Geo::Melissa::WebSmart;
+
+    my $request = {
+      id    => '9876543210',      # API authorization
+      a1    => '123 Main Street', # first address line
+      a2    => 'Suite 23',        # second address line, optional
+      city  => 'San Francisco',   # city
+      state => 'CA',              # state/province
+      ctry  => 'US',              # country, US or CA, optional
+      zip   => '93102',           # zip/postal code
+      # other options specified in the API docs
+      parse   => 1,               # request the parsed address elements
+      geocode => 1,               # request geocoding
+    };
+    $result = Geo::Melissa::WebSmart->query($request);
+
+    if ($result->address) {
+      print $result->address->{Address1};
+    }
+
+=head1 CLASS METHODS
+
+=head2 query HASHREF
+
+Send a request to the web service.  See 
+L<http://www.melissadata.com/manuals/dqt-websmart-addresscheck-reference-guide.pdf> for API documentation.
+
+Returns an object of class Geo::Melissa::WebSmart.
+
+=cut
+
+my $ua = LWP::UserAgent->new;
+my $addresscheck_uri =
+  'https://addresscheck.melissadata.net/v2/REST/Service.svc/doAddressCheck';
+my $geocoder_uri =
+  'https://geocoder.melissadata.net/v2/REST/Service.svc/doGeoCode';
+
+sub query {
+  my $class = shift;
+  my %opt;
+  if (ref $_[0] eq 'HASH') {
+    %opt = %{ $_[0] };
+  } else {
+    %opt = @_;
+  }
+
+  my $parse = delete($opt{parse});
+  if ( $parse ) {
+    $opt{'opt'} = 'True';
+  }
+  my $geocode = delete($opt{geocode});
+
+  my $uri = URI->new($addresscheck_uri);
+  $uri->query_form(%opt);
+  warn "Geo::Melissa::WebSmart->query\n$uri\n\n" if $DEBUG;
+  my $http_req = HTTP::Request->new(GET => $uri->as_string);
+  my $resp = $ua->request($http_req);
+  my $self = { addr_response => $resp };
+  bless $self, $class;
+  if ( $resp->is_success ) {
+    local $@;
+    my $root = eval { XML::LibXML->load_xml(string => $resp->content) };
+    if ($@) {
+      $self->message("Unable to parse XML response:\n$@");
+      return $self;
+    }
+    $root = $root->firstChild; # ResponseArray
+    my $data = treeify($root);
+    if (exists $data->{Record}) {
+      $self->address($data->{Address});
+      $self->code($data->{Record}->{Results});
+    } else {
+      $self->code($data->{Results});
+    }
+  } else {
+    $self->message( $resp->status_line );
+  }
+  if ( $geocode and $self->address and $self->address->{AddressKey} > 0 ) {
+    $uri = URI->new($geocoder_uri);
+    $uri->query_form(
+      id => $opt{id},
+      key => $self->address->{AddressKey},
+    );
+    my $http_req = HTTP::Request->new(GET => $uri->as_string);
+    my $resp = $ua->request($http_req);
+    $self->{geo_response} = $resp;
+    if ($resp->is_success) {
+      local $@;
+      my $root = eval { XML::LibXML->load_xml(string => $resp->content) };
+      if ($@) {
+        $self->message("Unable to parse XML response:\n$@");
+        return $self;
+      }
+      $root = $root->firstChild; # ResponseArray
+      my $data = treeify($root);
+      if (exists $data->{Record}) {
+        # merge results
+        $self->address( { %{ $self->address },
+                          %{ $data->{Record}->{Address} },
+                        } );
+        $self->code( $self->code . ',' . $data->{Record}->{Results} );
+      } else {
+        $self->code( $self->code . ',' . $data->{Results} );
+      }
+    } else {
+      $self->message( $resp->status_line );
+    }
+  }
+  $self;
+}
+
+sub treeify {
+  # safe in this case because the XML of the reply record has no duplicate
+  # element names, and is unordered
+  my $node = shift;
+  my $tree = {};
+  my $text = '';
+  foreach my $n ($node->childNodes) {
+    if ($n->isa('XML::LibXML::Text')) {
+      $text .= ' ' . $n->nodeValue;
+    } elsif ($n->isa('XML::LibXML::Node')) {
+      $tree->{$n->nodeName} = treeify($n);
+    }
+  }
+  $text =~ s/^\s*//;
+  $text =~ s/\s*$//;
+  if (keys %$tree) {
+    if ($text) {
+      $tree->{''} = $text;
+    }
+    return $tree;
+  } else {
+    return $text;
+  }
+}
+
+=head1 METHODS
+
+=head2 code
+
+Returns the result code(s) generated by the query, as a comma separated 
+list.
+
+=cut
+
+sub code {
+  my $self = shift;
+  if (@_) {
+    $self->{_code} = shift;
+  }
+  $self->{_code} || '';
+}
+
+=head2 message
+
+Sets/gets an explicit error status.
+
+=cut
+
+sub message {
+  my $self = shift;
+  if (@_) {
+    $self->{_message} = shift;
+  }
+  $self->{_message} || '';
+}
+
+=head2 status_message
+
+Returns a semi-readable description of the request status, including any
+error or warning messages.
+
+=cut
+
+sub status_message {
+  my $self = shift;
+  join("\n",
+    $self->message,
+    map { $self->result_string($_) }
+    split(',', $self->code)
+  );
+}
+
+=head2 result_string CODE
+
+Returns the string description of the result code CODE.  Melissa provides
+both short and long descriptions; this method returns both, separated by 
+a newline.
+
+=cut
+
+my %result_strings = (
+AS01 => "Address Verified\nThe address is valid and deliverable.",
+AS02 => "Default Address\nThe default building address was verified but the suite or apartment number is missing or invalid.",
+AS03 => "Non USPS Address\nThis address is not deliverable by USPS, but it exists.",
+AS09 => "Foreign Address\nAddress is in a non-supported country.",
+AS10 => "CMRA Address\nAddress is a Commercial Mail Receiving Agency (CMRA) like the UPS Store. These addresses include a Private Mail Box (PMB or #) number. U.S. only.",
+AS13 => "Address Updated By LACS\nLACSLink updated the address from a rural-style (RR 1 Box 2) to a city-style address (123 Main St).",
+AS14 => "Suite Appended\nSuiteLink appended a suite number using the address and company name.(US Only)",
+AS15 => "Apartment Appended\nAddressPlus appended an apartment number using the address and last name.",
+AS16 => "Vacant Address\nAddress has been unoccupied for more than 90 days.(US Only)",
+AS17 => "No Mail Delivery\nAddress does not receive mail at this time.(US Only)",
+AS18 => "DPV Locked Out\nDPV processing was terminated due to the detection of what is determined to be an artificially created adress.(US Only)",
+AS20 => "Deliverable only by USPS\nThis address can only receive mail delivered through USPS (ie. PO Box or a military address)",
+AS22 => "No Alternate Address Suggestion\nFound No alternate address suggestion found for this address.",
+AS23 => "Extraneous information found\nExtra information not used in verifying the address was found. They have been placed in the ParsedGarbage field.",
+AE01 => "Postal Code Error\nThe ZIP or Postal Code does not exist and could not be determined by the city/municipality and state/province.",
+AE02 => "Unknown Street\nThe street name was not be found.",
+AE03 => "Component Error\nEither the directionals (N, E, SW, etc) or the suffix (AVE, ST, BLVD) are missing or invalid.",
+AE04 => "Non-Deliverable\nThe physical location exists but there are no addresses on this side of the street.(US Only)",
+AE05 => "Multiple Match\nInput matched to multiple addresses and there is not enough information to break the tie.",
+AE06 => "Early Warning System\nThis address cannot be verified now but will be at a future date.(US Only)",
+AE07 => "Minimum Address\nThe required combination of address/city/state or address/zip is missing.",
+AE08 => "Suite/Apartment Invalid\nThe suite or apartment number is not valid.",
+AE09 => "Suite/Apartment Missing\nThe suite or apartment number is missing.",
+AE10 => "House/Building Number Invalid\nThe address number in the input address is not valid.",
+AE11 => "House/Building Number Missing\nThe address number in the input address is missing.",
+AE12 => "Box Number Invalid\nThe input address box number is invalid.",
+AE13 => "Box Number Missing\nThe input address box number is missing.",
+AE14 => "PMB number Missing\nThe address is a Commercial Mail Receiving Agency (CMRA) and the Private Mail Box (PMB or #) number is missing.",
+AE15 => "Demo Mode\nLimited to Demo Mode operation.",
+AE16 => "Expired Database\nThe Database has expired.",
+AE17 => "Suite/Apartment Not Required\nAddress does not have Suites or Apartments.",
+AE19 => "Find Suggestion Timeout\nFindSuggestion function has exceeded time limit.",
+AE20 => "Find Suggestion Disabled\nFindSuggestion function is disabled, see manual for details.",
+AC01 => "ZIP Code Change\nThe ZIP Code was changed or added.",
+AC02 => "State Change\nThe State abbreviation was changed or added.",
+AC03 => "City Change\nThe City name was changed or added.",
+AC04 => "Address Base Alternate Change\nThe address was found to be an alternate record and changed to the base (preferred) version.",
+AC05 => "Alias Name change\nThe street name was changed from a former or nickname street to the USPS preferred street name.",
+AC06 => "Address Swapped\nAddress1 was swapped with Address2 (only Address2 contained a valid address).",
+AC07 => "Address1 & Company Swapped\nAddress1 and Company were swapped (only Company contained a valid address).",
+AC08 => "Plus4 Change\nA non-empty Plus4 was changed.",
+AC09 => "Urbanization Change\nThe Urbanization was changed.",
+AC10 => "Street Name Change\nThe street name was changed",
+AC11 => "Street Suffix Change\nThe street suffix was changed",
+AC12 => "Street Predirection or Postdirection Change\nThe street predirection or postdirection was changed",
+AC13 => "Suite Name Change\nThe suite name was changed",
+AC14 => "Suite Range Change\nThe secondary unit number was changed or appended.",
+);
+
+my %general_errors = (
+  SE01 => "Web Service Internal Error\nThe web service experienced an internal error.",
+  GE01 => "Empty Request Structure\nThe SOAP, JSON, or XML request structure is empty.",
+  GE02 => "Empty Request Record Structure\nThe SOAP, JSON, or XML request record structure is empty.",
+  GE03 => "Records Per Request Exceeded\nThe counted records sent more than the number of records allowed per request.",
+  GE04 => "Empty CustomerID\nThe CustomerID is empty.",
+  GE05 => "Invalid CustomerID\nThe CustomerID is invalid.",
+  GE06 => "Disabled CustomerID\nThe CustomerID is disabled.",
+  GE07 => "Invalid Request\nThe SOAP, JSON, or XML request is invalid.",
+);
+
+
+sub result_string {
+  my ($class, $code) = @_;
+  $result_strings{$code};
+}
+
+=head2 address
+
+Returns a hashref of the fields under the Address element of the response.
+
+=cut
+
+sub address {
+  my $self = shift;
+  if ( @_ ) {
+    $self->{_address} = shift;
+  }
+  $self->{_address} ||= {};
+}
+
+=head1 AUTHOR
+
+Mark Wells, C<< <mark at freeside.biz> >>
+
+=head1 SUPPORT
+
+For all allowed query parameters, and details on the 'address' data structure,
+see the Melissa WebSmart documentation:
+  L<http://www.melissadata.com/manuals/dqt-websmart-addresscheck-reference-guide.pdf>
+
+Commercial support for this module is available from Freeside Internet 
+Services:
+
+    L<http://www.freeside.biz/>
+
+=back
+
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright (C) 2013 Freeside Internet Services, Inc.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
+=cut
+
+1;
diff --git a/ignore.txt b/ignore.txt
new file mode 100644 (file)
index 0000000..a21fbea
--- /dev/null
@@ -0,0 +1,12 @@
+blib*
+Makefile
+Makefile.old
+Build
+Build.bat
+_build*
+pm_to_blib*
+*.tar.gz
+.lwpcookies
+cover_db
+pod2htm*.tmp
+Geo-Melissa-WebSmart-*
diff --git a/t/00-load.t b/t/00-load.t
new file mode 100644 (file)
index 0000000..ad9f438
--- /dev/null
@@ -0,0 +1,9 @@
+#!perl -T
+
+use Test::More tests => 1;
+
+BEGIN {
+    use_ok( 'Geo::Melissa::WebSmart' ) || print "Bail out!\n";
+}
+
+diag( "Testing Geo::Melissa::WebSmart $Geo::Melissa::WebSmart::VERSION, Perl $], $^X" );