├── .github └── workflows │ └── dzil-build-and-test.yml ├── .gitignore ├── .mailmap ├── Changes ├── MANIFEST.SKIP ├── README.pod ├── cpanfile ├── dist.ini ├── examples ├── agg.pl ├── author-country.pl ├── author.pl ├── author_releases.pl ├── authors_blogs.pl ├── autocomplete.pl ├── autocomplete_suggest.pl ├── changes.pl ├── complex-either-and.pl ├── complex-either-not.pl ├── complex-nested-either-and.pl ├── complex.pl ├── contributors.pl ├── cover.pl ├── distribution.pl ├── download_url.pl ├── es_filter.pl ├── fields-filter.pl ├── metacpan_url.pl ├── mirror.pl ├── module.pl ├── package.pl ├── permission.pl ├── pod.pl ├── rating.pl ├── recent.pl ├── recent_today.pl ├── release.pl ├── rev_deps-recursive.pl ├── rev_deps.pl ├── top20_favorites.pl └── totals.pl ├── lib └── MetaCPAN │ ├── Client.pm │ └── Client │ ├── Author.pm │ ├── Cover.pm │ ├── Distribution.pm │ ├── DownloadURL.pm │ ├── Favorite.pm │ ├── File.pm │ ├── Mirror.pm │ ├── Module.pm │ ├── Package.pm │ ├── Permission.pm │ ├── Pod.pm │ ├── Rating.pm │ ├── Release.pm │ ├── Request.pm │ ├── ResultSet.pm │ ├── Role │ ├── Entity.pm │ └── HasUA.pm │ ├── Scroll.pm │ └── Types.pm └── t ├── api ├── _get.t ├── _get_or_search.t ├── _search.t ├── author.t ├── cover.t ├── distribution.t ├── download_url.t ├── favorite.t ├── file.t ├── module.t ├── package.t ├── permission.t ├── pod.t ├── rating.t ├── release.t └── reverse-dependencies.t ├── entity.t ├── lib └── Functions.pm ├── request.t ├── result_custom.t ├── resultset.t ├── scroll.t └── ua_trap.t /.github/workflows/dzil-build-and-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: dzil build and test 3 | 4 | on: 5 | push: 6 | branches: 7 | - "*" 8 | pull_request: 9 | branches: 10 | - "*" 11 | schedule: 12 | - cron: "15 4 * * 0" # Every Sunday morning 13 | 14 | jobs: 15 | build-job: 16 | name: Build distribution 17 | runs-on: ubuntu-latest 18 | container: 19 | image: perldocker/perl-tester:5.32 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Run Tests 23 | env: 24 | AUTHOR_TESTING: 1 25 | AUTOMATED_TESTING: 1 26 | EXTENDED_TESTING: 1 27 | RELEASE_TESTING: 1 28 | run: auto-build-and-test-dist 29 | - uses: actions/upload-artifact@master 30 | with: 31 | name: build_dir 32 | path: build_dir 33 | if: ${{ github.actor != 'nektos/act' }} 34 | coverage-job: 35 | needs: build-job 36 | runs-on: ubuntu-latest 37 | container: 38 | image: perldocker/perl-tester:5.32 39 | steps: 40 | - uses: actions/checkout@v2 # codecov wants to be inside a Git repository 41 | - uses: actions/download-artifact@master 42 | with: 43 | name: build_dir 44 | path: . 45 | - name: Install deps and test 46 | run: cpan-install-dist-deps && test-dist 47 | env: 48 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 49 | test-job: 50 | needs: build-job 51 | runs-on: ${{ matrix.os }} 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | os: [ubuntu-latest, macos-latest, windows-latest] 56 | perl-version: 57 | - "5.10" 58 | - "5.12" 59 | - "5.14" 60 | - "5.16" 61 | - "5.18" 62 | - "5.20" 63 | - "5.22" 64 | - "5.24" 65 | - "5.26" 66 | - "5.28" 67 | - "5.30" 68 | - "5.32" 69 | exclude: 70 | - os: windows-latest 71 | perl-version: "5.10" 72 | - os: windows-latest 73 | perl-version: "5.12" 74 | - os: windows-latest 75 | perl-version: "5.32" 76 | name: Perl ${{ matrix.perl-version }} on ${{ matrix.os }} 77 | steps: 78 | - name: Set Up Perl 79 | uses: shogo82148/actions-setup-perl@v1 80 | with: 81 | perl-version: ${{ matrix.perl-version }} 82 | distribution: strawberry # this option only used on Windows 83 | - uses: actions/download-artifact@master 84 | with: 85 | name: build_dir 86 | path: . 87 | - name: install deps using cpm 88 | uses: perl-actions/install-with-cpm@v1 89 | with: 90 | cpanfile: "cpanfile" 91 | args: "--with-suggests --with-recommends --with-test" 92 | - run: prove -l t xt 93 | env: 94 | AUTHOR_TESTING: 1 95 | RELEASE_TESTING: 1 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Build 2 | Build.PL 3 | Build.bat 4 | LICENSE 5 | MANIFEST 6 | MANIFEST.bak 7 | META.* 8 | MYMETA.* 9 | Makefile 10 | Makefile.PL 11 | Makefile.old 12 | MetaCPAN-Client-* 13 | blib/ 14 | .build/ 15 | _build/ 16 | cover_db/ 17 | cpanfile.snapshot 18 | inc/ 19 | .last_cover_stats 20 | local/ 21 | .lwpcookies 22 | nytprof.out 23 | pm_to_blib 24 | pod2htm*.tmp 25 | *.swp 26 | tidyall.ERR 27 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html 2 | 3 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for MetaCPAN-Client (previously MetaCPAN-API) 2 | 3 | 2.033000 25.11.24 4 | * Remove backpan_directory option (Leo Lapworth, GH#127) 5 | 6 | 2.032000 15.05.24 7 | * Fix scroller issues (haarg, GH#123) 8 | * Removed rating fetching (haarg, GH#124) 9 | * Fix example script (Mickey) 10 | 11 | 2.031001 11.03.24 12 | * Show a real error for internal errors (haarg) 13 | 14 | 2.031000 31.10.23 15 | * Fix reverse-dependencies distributions check (Mickey) 16 | * 'use Data::Printer' instead of shortened 'use DDP' (Mickey) 17 | 18 | 2.030000 22.08.22 19 | * Set verify_SSL=>1 for default HTTP::Tiny user agent (Stig Palmquist, GH#113) 20 | * Updated docs (Dave Rolsky, GH#111) 21 | 22 | 2.029000 20.12.20 23 | * Added checksum_sha256 & checksum_md5 fields support (stigo, GH#110) 24 | * Cleanup old files (Mickey) 25 | 26 | 2.028000 24.8.20 27 | * Support specific versions in download_url (Nicolas R (atoomic), GH#107) 28 | 29 | 2.027000 11.8.20 30 | * Run Travis tests with more Perls (Olaf Alders, GH#102) 31 | 32 | 2.026000 14.3.19 33 | * Added (back, syntax modified for ES2.x) example script 34 | top20_favorites (Mickey) 35 | * Updated SYNOPSIS for Favorite (Mickey, Olaf Alders) 36 | * Fixed link to Search Spec (Renee Baecker, GH#101) 37 | * Fixed typo in error message (Johann Rolschewski, GH#100) 38 | 39 | 2.025000 22.4.18 40 | * Added support for the new 'cover' index - cpancover.org info (Mickey) 41 | 42 | 2.024000 20.4.18 43 | * Fix warning on a JSON::PP::Boolean check (Mickey) 44 | 45 | 2.023000 26.1.18 46 | * Support the new 'deprecated' field in File and Release types (Mickey) 47 | 48 | 2.022000 3.1.18 49 | * Allow user-defined target classes in ResultSet (Kent Fredric, Sawyer) 50 | * Added test for reverse dependencies (Sawyer) 51 | * Switched ref() checks to Ref::Util::is_ref (Mickey) 52 | 53 | 2.021000 18.11.17 54 | * Scroller fix for page skipping (Thomas Sibley) 55 | * Sorting in scrolled searches (Thomas Sibley) 56 | * Type check cleanup (Thomas Sibley) 57 | 58 | 2.020000 17.11.17 59 | * Added support for /search/autocomplete/suggest (Mickey) 60 | 61 | 2.019000 16.11.17 62 | * Added 'package' type support for scrolled searches (Mickey) 63 | 64 | 2.018000 16.10.17 65 | * Fix fetch URL (Mickey, GH#92) 66 | * Removed critic author test (Mickey) 67 | 68 | 2.017000 25.6.17 69 | * reverse_dependencies: update link to new API endpoint (Mickey, GH#89) 70 | 71 | 2.016000 7.6.17 72 | * Support CSV field list in 'all' requests (Mickey, GH#87) 73 | 74 | 2.015000 14.5.17 75 | * Added 'main_module' field to the Release object (Mickey) 76 | * Updated doc (Matthew Horsfall, GH#85) 77 | 78 | 2.014000 12.5.17 79 | * Fixed single-value case for expected arrayref (Mickey, GH#84) 80 | * Added support for new release/contributors endpoint (Mickey) 81 | 82 | 2.013001 12.5.17 83 | * Updated endpoint name following API change (Mickey) 84 | 85 | 2.013000 9.5.17 86 | * Added support for new 'packages' type (Mickey) 87 | 88 | 2.012000 27.4.17 89 | * Fixed 'email' field handling in Author objects (Mickey, GH#83) 90 | 91 | 2.011000 18.4.17 92 | * Added support for scroller time/size params (Mickey) 93 | * Removed warning of scroller deletion failure (Mickey, GH#81) 94 | 95 | 2.010000 3.4.17 96 | * Added support for new 'permission' type (Mickey) 97 | 98 | 2.009001 29.3.17 99 | * Use Test::Needs to force a minimum 100 | WWW::Mechanize::Cached version (Olaf Alders, GH#76) 101 | 102 | 2.009000 24.3.17 103 | * Bump WWW::Mechanize::Cached version to 1.50 (Olaf Alders, GH#76) 104 | * Require LWP::Protocol::https in tests (Mickey, GH#79) 105 | * Added 'changes' method for Release objects (Mickey, GH#57) 106 | * Cleaner URLs - removed redundant slashes and 'v1' (Mickey) 107 | * Created a role for user-agent handling for reuse (Mickey) 108 | 109 | 2.008001 23.3.17 110 | * Fixed a test (Mickey) 111 | 112 | 2.008000 22.3.17 113 | * Added metacpan_url method to the entity objects 114 | (Mickey, #GH69) 115 | 116 | 2.007000 8.3.17 117 | * Update tests for newer Perl versions, to run without 118 | '.' in @INC (Sawyer X, GH#72) 119 | 120 | 2.006000 24.2.17 121 | * Support '_source' filtering (Mickey, GH#70) 122 | * Support debug-mode for detailed error messages (Mickey) 123 | 124 | 2.005000 13.2.17 125 | * Added the ascii_name and perlmongers fields to the Author object 126 | (Dave Rolsky, GH #66) 127 | * Fixed Author->dir to actually return something (Dave Rolsky, GH 128 | #66) 129 | 130 | 2.004000 30.12.16 131 | * Speed up own scroller (Mickey) 132 | * Fixed rev_deps (Mickey) 133 | 134 | 2.004000-TRIAL 24.12.16 135 | * Removed dependency: Search::Elasticsearch 136 | in favor of an internal scroller (Mickey) 137 | * Added Types class for 'isa' checks (Mickey) 138 | 139 | 2.003000 19.12.16 140 | * Escaped query to autocomplete (Mickey) 141 | * Removed dependency: Try::Tiny (Mickey) 142 | 143 | 2.002000 14.12.16 144 | * Support 'autocomplete' endpoint (Mickey) 145 | 146 | 2.001000 08.12.16 147 | * Distribution: added 'rt' & 'github' methods (Mickey) 148 | * Use Ref::Util for ref checks (Mickey) 149 | 150 | 2.000000 18.11.16 151 | * Major version: v1 full support 152 | - removed support and default settings for v0 153 | - corrected domain, base_url setting, using clientinfo 154 | - code/tests updates and cleanup 155 | (Mickey, Brad Lhotsky) 156 | * Pinned Search::Elasticsearch version to 2.03 (Mickey) 157 | * Use @Starter in dist.ini + cpanfile cleanup (Grinnz) 158 | 159 | 1.028003 23.10.16 160 | * Removed AutoPrereqs from dist.ini (Mickey) 161 | 162 | 1.028002 23.10.16 163 | * GH #53 a few small dist.ini tweaks (Karen Etheridge) 164 | * Even more dist.ini tweaks (Mickey, thanks to Grinnz) 165 | 166 | 1.028001 22.10.16 167 | * GH #51 Adds eumm_version to dist.ini (Olaf Alders) 168 | * GH #52 Stop excluding cpanfile from being copied to 169 | build (Olaf Alders) 170 | 171 | 1.028000 21.10.16 172 | * GH #50 Remove hard-deps for HTTP::Tiny::Mech and 173 | WWW::Mechanize::Cached (Paul Howarth) 174 | * dist.ini: don't automatically update cpanfile (Mickey) 175 | 176 | 1.027000 20.10.16 177 | * GH #49 Convert values of JSON::PP::Boolean objects in output 178 | so they are not skipped when expeting scalars (Mickey) 179 | 180 | 1.026001 19.10.16 181 | * Fixed version range for Search::Elasticsearch (Mickey) 182 | 183 | 1.026000 19.10.16 184 | * Moved distini prereqs to cpanfile (Mickey) 185 | * Limit Search::Elasticsearch version to 2.02 (Mickey) 186 | * Updated docs (Thomas Sibley) 187 | 188 | 1.025000 30.8.16 189 | * Added some version requirements to improve SSL over 190 | HTTP::Tiny (Mickey) 191 | * Added default values for distribution keys with no content 192 | (Mickey, per Tux request) 193 | 194 | 1.024000 28.08.16 195 | * Try to fetch clientinfo from https://clientinfo.metacpan.org 196 | to get default production version (Mickey) 197 | 198 | 1.023000 27.08.16 199 | * Added support for version by env METACPAN_VERSION (Mickey) 200 | * Added tests for version argument (Mickey) 201 | 202 | 1.022003 06.08.16 203 | * Fixed a warning in $file->pod (Mickey) 204 | 205 | 1.022002 06.08.16 206 | * Added LWP::Protocol::https as test dependency (Mickey) 207 | 208 | 1.022001 05.08.16 209 | * check user provided UA for 'get' and 'post' methods (Mickey) 210 | * document updates (Mickey) 211 | 212 | 1.022000 04.08.16 213 | * Rework type checking - enforce expected types, inc. 214 | single-valued array-ref unwrapping; doesn't break 215 | types that are expected to be array-refs (Mickey) 216 | 217 | 1.021000 27.07.16 218 | * Fix result values in v1 - single valued arrayref in ES 219 | result will be turned to a scalar (Mickey) 220 | 221 | 1.020000 12.07.16 222 | * Added support for Author->release_count & Author->links methods (Mickey) 223 | * Added support for url_prefix parameter for Pod (Mickey) 224 | 225 | 1.019000 06.07.16 226 | * Added missing 'download_url' attribute to file/module 227 | result objects (Mickey) 228 | 229 | 1.018000 06.07.16 230 | * Added support for download_url endpoint (Mickey) 231 | * Default domain set by providing 'version' - 232 | makes it easy to work with v1 (Mickey) 233 | 234 | 1.017000 28.06.16 235 | * Fixed nodes list for Search::Elasticsearch (Mickey) 236 | * Added support for 'aggregations' (Mickey) 237 | 238 | 1.016000 27.06.16 239 | * Added support for 'all' filters type 'files' (Mickey) 240 | * http -> https (Mickey) 241 | 242 | 1.015000 02.06.16 243 | * Adding `source` method to MetaCPAN::Client::File (stevan) 244 | 245 | 1.014000 29.04.16 246 | * Fix warning on missing fields param (Mickey, Sawyer X) 247 | * Switch to Search::Elasticsearch 2.0. (Sawyer X) 248 | * You can test MetaCPAN::API with a different domain using the 249 | environment variable "METACPAN_DOMAIN". (Mickey) 250 | 251 | 1.013000 25.04.15 252 | * GH #34 Use Travis for CI (oalders) 253 | * GH #35 Improve Kwalitee + test improvements (oalders) 254 | 255 | 1.012000 09.04.15 256 | * GH #33 added Mirror type and support for mirrors search in 'all' queries (mickeyn) 257 | * GH #33 support 'ratings' search in 'all' queries (mickeyn) 258 | * more example scripts: facets, top favorites, all authors blogs (mickeyn) 259 | * cleanup & doc updates (Gabor Szabo, mickeyn) 260 | 261 | 1.011000 27.01.15 262 | * support 'favorites' type and 'facets' key param in 'all' queries (mickeyn) 263 | 264 | 1.010000 23.01.15 265 | * support wildcard-only value in complex search (mickeyn) 266 | * support raw Elasticsearch filters in 'all' queries (mickeyn) 267 | 268 | 1.009000 11.01.15 269 | * GH #25 (RT #99499): added support for 'fields' filtering (mickeyn, oalders) 270 | 271 | 1.008001 01.01.15 272 | * Happy new year! 273 | * Correct Meta resources for the repo. 274 | * Correct link in POD for the Pod element. (Alex Vandiver) 275 | 276 | 1.008000 22.11.14 277 | * RT #99498: added API for 'match_all' queries via all($type) (oalders, mickeyn) 278 | * GH #21: make 'domain' and 'version' settable via new() (oalders) 279 | * RT #94491: document nested queries (neilb, mickeyn) 280 | 281 | 1.007001 09.10.14 282 | * GH #18: HTTP::Tiny::Mech and WWW::Mechanize::Cached downgraded to being non-essential for tests (kentnl) 283 | * GH #19: Include 'metadata' in known_fields for ::Release (kentnl) 284 | 285 | 1.007000 14.08.14 286 | * Ensure passing user specified ua values to all parts internally, 287 | including to Elasticsearch (kentnl) GH #17 RT#95796 288 | * Entity consuming roles now have a 'client' attribute which will lazy build, 289 | or reference the MetaCPAN::Client that created them via new_from_request (kentnl) GH #17 290 | 291 | 1.006000 24.06.14 292 | * Add 'recent' functionality (latest releases) 293 | 294 | 1.005000 09.06.14 295 | * Add Pod object to allow direct POD fetching (reneeb) 296 | * Support single element without wrapping arrayref in structures 297 | * Updated documents - basic/complex search links and wording (tsibley) 298 | 299 | 1.004001 27.05.14 300 | * correct rev_deps query 301 | 302 | 1.004000 27.05.14 303 | * reworked ResultSet to allow RS in non-scrolled searches. 304 | 305 | 1.003000 05.05.14 306 | * Add proper POD fetching from module/file objects. 307 | * GH #1: Switch from JSON.pm to JSON::MaybeXS. 308 | * GH #2: Remove incorrect and unnecessary check for class names. 309 | * Provide "ua" attribute in the main object to override user agent. 310 | * Add some use-case examples (examples directory). 311 | * Add 'releases' method to Author (not official so no docs yet). 312 | * GH #4: Use example with hyphen. 313 | * Related to GH #4, use Data::Printer instead of shotened name "DDP". 314 | 315 | 1.002000 24.04.14 316 | * Add 'not' support for complex queries 317 | * Add reverse_dependencies method 318 | 319 | 1.001001 15.04.14 320 | * Fix the reading of scroller result when 'fields' param is passed. 321 | 322 | 1.001000 09.04.14 323 | * Add support for nested either/all queries 324 | * Add tests for complex queries (two levels deep) 325 | * Correct documentation on complex queries 326 | * Update tests to work on older versions of perl 327 | 328 | 1.000001 03.04.14 329 | * changed Elasticsearch (deprecated) to Search::Elasticsearch (official) 330 | 331 | 1.000000 02.04.14 332 | ** Completely rewritten ** 333 | MetaCPAN::API has been completely rewritten as MetaCPAN::Client. 334 | 335 | Other than the different name (to match MetaCPAN itself), the 336 | following changes had been made: 337 | * MetaCPAN::Client is officially part of MetaCPAN 338 | * Semantic Versioning (semver) scheme 339 | * Moo as object system 340 | * All entities are now objects 341 | * Using Elasticsearch.pm for complex queries 342 | * Rich syntax for nested queries (AND/OR) 343 | * Simple queries return entity objects 344 | * Complex queries return resultset objects (with iterator) 345 | * Support for scrolled searches 346 | * Inline support for Elasticsearch facets 347 | * Documentation, tests - all cleaned, rewritten 348 | 349 | 0.43 05.04.12 350 | * Add example in POD of advanced usage with cache by Kent Fredric. 351 | (Gist: https://gist.github.com/1291928) 352 | * Sort keys in param join - more predictable result. 353 | 354 | 0.42 08.01.12 355 | * Corrected documentation in MetaCPAN::API::Source. 356 | * Updated Dist::Zilla configuration, added more tests. 357 | 358 | 0.41 07.01.12 359 | * Use Test::TinyMocker 0.02 syntax in tests to avoid test fails. 360 | 361 | 0.40 06.01.12 362 | * Fixed JSON encoding so ElasticSearch won't fail. 363 | (Christian Walde) 364 | * Documentation fixes (Logan - logie17). 365 | 366 | 0.34 02.10.11 367 | * Added MetaCPAN::API::Source (Renee Baecker). 368 | * Fix of HTTP::Tiny content-type in options (Renee Baecker). 369 | * Typo fix (Olaf Alders, reported by @doherty). 370 | 371 | 0.33 24.08.11 372 | * No functional changes. 373 | * Skip t::lib in dzil. 374 | 375 | 0.32 04.08.11 376 | * Use a default agent string for requests. 377 | * Use a default agent string for tests. 378 | 379 | 0.31 02.08.11 380 | * URL updates (thanks to Olaf Alders, OALDERS). 381 | * Small mismatched quote in POD example. 382 | 383 | 0.30 30.07.11 384 | * Add POST query searches (RT #69814, GH #2). 385 | (original code by Tim Bunce, thank you!) 386 | * More tests. 387 | 388 | 0.20 28.07.11 389 | * Add complex (manual) searches to author()/release() + docs. 390 | * Add file() as a synonym to module(). 391 | * Respect content-type. 392 | * Allow setting additional params to fetch(). 393 | * Allow "pauseid" in author via key. 394 | * Better check for content-type. 395 | 396 | 0.11 24.07.11 397 | * Correct the POD example and tests. 398 | * Update to use a different API path. 399 | 400 | 0.10 24.07.11 401 | * Almost complete rewrite. 402 | * Make use of the new beta API. 403 | * Remove old API support. 404 | * Remove DWIM methods for now. 405 | * Include lots of tests. 406 | 407 | 0.02 13.02.11 408 | (First stable release!) 409 | * Add docs (Sawyer X). 410 | 411 | 0.01_03 10.02.11 412 | * Immutable base_url (Olaf Alders, Sawyer X). 413 | 414 | 0.01_02 10.02.11 415 | * Make base_url 'rw' (Olaf Alders). 416 | * Update module search URL (Olaf Alders). 417 | * Refactoring using _http_req method (Sawyer X). 418 | * Remove render_result method (Sawyer X). 419 | * Remove unnecessary print (Sawyer X). 420 | 421 | 0.01_01 05.02.11 422 | * Module, Dist, POD, Author and CPANRatings are supported 100%. 423 | * Still needs more refactoring, and some methods aren't used yet. 424 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | cpanfile.snapshot 2 | local 3 | MetaCPAN-Client* 4 | -------------------------------------------------------------------------------- /README.pod: -------------------------------------------------------------------------------- 1 | =pod 2 | 3 | =encoding UTF-8 4 | 5 | =head1 NAME 6 | 7 | MetaCPAN::Client - A comprehensive, DWIM-featured client to the MetaCPAN API 8 | 9 | =head1 VERSION 10 | 11 | version 2.033000 12 | 13 | =head1 SYNOPSIS 14 | 15 | # simple usage 16 | my $mcpan = MetaCPAN::Client->new(); 17 | my $author = $mcpan->author('XSAWYERX'); 18 | my $dist = $mcpan->distribution('MetaCPAN-Client'); 19 | 20 | # advanced usage with cache (contributed by Kent Fredric) 21 | use CHI; 22 | use WWW::Mechanize::Cached; 23 | use HTTP::Tiny::Mech; 24 | use MetaCPAN::Client; 25 | 26 | my $mcpan = MetaCPAN::Client->new( 27 | ua => HTTP::Tiny::Mech->new( 28 | mechua => WWW::Mechanize::Cached->new( 29 | cache => CHI->new( 30 | driver => 'File', 31 | root_dir => '/tmp/metacpan-cache', 32 | ), 33 | ), 34 | ), 35 | ); 36 | 37 | # now $mcpan caches results 38 | 39 | =head1 DESCRIPTION 40 | 41 | This is a hopefully-complete API-compliant client to MetaCPAN 42 | (L) with DWIM capabilities, to make your life easier. 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 request 47 | 48 | Internal attribute representing the request object making the request to 49 | MetaCPAN and analyzing the results. You probably don't want to set this, nor 50 | should you have any usage of it. 51 | 52 | =head2 ua 53 | 54 | If provided, L will use the user agent object 55 | instead of the default, which is L. 56 | 57 | Then it can be used to fetch the user agent object used by 58 | L. 59 | 60 | =head2 domain 61 | 62 | If given, will be used to alter the API domain. 63 | 64 | =head2 debug 65 | 66 | If given, errors will include some low-level detailed message. 67 | 68 | =head1 METHODS 69 | 70 | =head2 author 71 | 72 | my $author = $mcpan->author('XSAWYERX'); 73 | my $author = $mcpan->author($search_spec); 74 | 75 | Finds an author by either its PAUSE ID or by a search spec defined by a hash 76 | reference. Since it is common to many other searches, it is explained below 77 | under C. 78 | 79 | Returns a L object on a simple search (PAUSE ID), or 80 | a L object populated with 81 | L objects on a complex (L) search. 82 | 83 | =head2 cover 84 | 85 | my $cover = $mcpan->cover('Moose-2.2007'); 86 | 87 | Returns a L object. 88 | 89 | =head2 distribution 90 | 91 | my $dist = $mcpan->distribution('MetaCPAN-Client'); 92 | my $dist = $mcpan->distribution($search_spec); 93 | 94 | Finds a distribution by either its distribution name or by a search spec 95 | defined by a hash reference. Since it is common to many other searches, it is 96 | explained below under C. 97 | 98 | Returns a L object on a simple search 99 | (distribution name), or a L object populated with 100 | L objects on a complex (L) 101 | search. 102 | 103 | =head2 file 104 | 105 | Returns a L object. 106 | 107 | =head2 favorite 108 | 109 | my $favorite = $mcpan->favorite({ distribution => 'Moose' }); 110 | 111 | Returns a L object containing 112 | L results. 113 | 114 | =head2 rating 115 | 116 | my $rating = $mcpan->rating({ distribution => 'Moose' }); 117 | 118 | Returns a L object containing 119 | L results. 120 | 121 | =head2 release 122 | 123 | my $release = $mcpan->release('MetaCPAN-Client'); 124 | my $release = $mcpan->release($search_spec); 125 | 126 | Finds a release by either its distribution name or by a search spec defined by 127 | a hash reference. Since it is common to many other searches, it is explained 128 | below under C. 129 | 130 | Returns a L object on a simple search (release name), 131 | or a L object populated with 132 | L objects on a complex (L) search. 133 | 134 | =head2 mirror 135 | 136 | my $mirror = $mcpan->mirror('kr.freebsd.org'); 137 | 138 | Returns a L object. 139 | 140 | =head2 module 141 | 142 | my $module = $mcpan->module('MetaCPAN::Client'); 143 | my $module = $mcpan->module($search_spec); 144 | 145 | Finds a module by either its module name or by a search spec defined by a hash 146 | reference. Since it is common to many other searches, it is explained below 147 | under C. 148 | 149 | Returns a L object on a simple search (module name), or 150 | a L object populated with 151 | L objects on a complex (L) search. 152 | 153 | =head2 package 154 | 155 | my $package = $mcpan->package('MooseX::Types'); 156 | 157 | Returns a L object. 158 | 159 | =head2 permission 160 | 161 | my $permission = $mcpan->permission('MooseX::Types'); 162 | 163 | Returns a L object. 164 | 165 | =head2 reverse_dependencies 166 | 167 | my $deps = $mcpan->reverse_dependencies('Search::Elasticsearch'); 168 | 169 | all L objects of releases that are directly 170 | dependent on a given module, returned as L. 171 | 172 | =head2 rev_deps 173 | 174 | Alias to C described above. 175 | 176 | =head2 autocomplete 177 | 178 | my $ac = $mcpan->autocomplete('Danc'); 179 | 180 | Call the search/autocomplete endpoint with a query string. 181 | 182 | Returns an array reference. 183 | 184 | =head2 autocomplete_suggest 185 | 186 | my $ac = $mcpan->autocomplete_suggest('Moo'); 187 | 188 | Call the search/autocomplete/suggest endpoint with a query string. 189 | 190 | Returns an array reference. 191 | 192 | =head2 recent 193 | 194 | my $recent = $mcpan->recent(10); 195 | my $recent = $mcpan->recent('today'); 196 | 197 | return the latest N releases, or all releases from today. 198 | 199 | returns a L of L. 200 | 201 | =head2 pod 202 | 203 | Get POD for given file/module name. 204 | returns a L object, which supports various output 205 | formats (html, plain, x_pod & x_markdown). 206 | 207 | my $pod = $mcpan->pod('Moo')->html; 208 | my $pod = $mcpan->pod('Moo', { url_prefix => $prefix })->html; 209 | 210 | =head2 download_url 211 | 212 | Retrieve information from the 'download_url' endpoint 213 | 214 | my $download_url = $mcpan->download_url($distro, [$version_or_range, $dev]); 215 | 216 | # request the last available version 217 | my $download_url = $mcpan->download_url('Moose'); 218 | 219 | # request an older version 220 | my $download_url = $mcpan->download_url('Moose', '1.01'); 221 | 222 | # using a range 223 | my $download_url = $mcpan->download_url('Moose', '<=1.01'); 224 | my $download_url = $mcpan->download_url('Moose', '>1.01,<=2.00'); 225 | 226 | Range operators are '== != <= >= < > !'. 227 | You can use a comma ',' to add multiple rules. 228 | 229 | # requesting dev release 230 | my $download_url = $mcpan->download_url('Moose', '>1.01', 1); 231 | 232 | Returns a L object 233 | 234 | =head2 all 235 | 236 | Retrieve all matches for authors/modules/distributions/favorites or releases. 237 | 238 | my $all_releases = $mcpan->all('releases') 239 | 240 | When called with a second parameter containing a hash ref, 241 | will support the following keys: 242 | 243 | =head3 fields 244 | 245 | See SEARCH PARAMS. 246 | 247 | my $all_releases = $mcpan->all('releases', { fields => [...] }) 248 | 249 | =head3 _source 250 | 251 | See SEARCH PARAMS. 252 | 253 | my $all_releases = $mcpan->all('releases', { _source => [...] }) 254 | 255 | =head3 es_filter 256 | 257 | Pass a raw Elasticsearch filter structure to reduce the number 258 | of elements returned by the query. 259 | 260 | my $some_releases = $mcpan->all('releases', { es_filter => {...} }) 261 | 262 | =head2 BUILDARGS 263 | 264 | Internal construction wrapper. Do not use. 265 | 266 | =head1 SEARCH PARAMS 267 | 268 | Most searches take params as an optional hash-ref argument. 269 | these params will be passed to the search action. 270 | 271 | In non-scrolled searches, 'fields' filter is the only supported 272 | parameter ATM. 273 | 274 | =head2 fields 275 | 276 | Filter the fields to reduce the amount of data pulled from MetaCPAN. 277 | can be passed as a csv list or an array ref. 278 | 279 | my $module = $mcpan->module('Moose', { fields => "version,author" }); 280 | my $module = $mcpan->module('Moose', { fields => [qw/version author/] }); 281 | 282 | =head2 _source 283 | 284 | Note: this param and its description are a bit too Elasticsearch specific. 285 | just like 'es_filter' - use only if you know what you're dealing with. 286 | 287 | Some fields are not indexed in Elasticsearch but stored as part of 288 | the entire document. 289 | 290 | These fields can still be read, but without the internal Elasticsearch 291 | optimizations and the server will internally read the whole document. 292 | 293 | Why do we even need those? because we don't index everything and some things 294 | we can't to begin with (like non-leaf fields that hold a structure) 295 | 296 | my $module = $mcpan->all('releases', { _source => "stat" }); 297 | 298 | =head2 scroller_time 299 | 300 | Note: please use with caution. 301 | 302 | This parameter will set the maximum lifetime of the Elasticsearch scroller on 303 | the server (default = '5m'). Normally you do not need to set this value (as 304 | tweaking this value can affect resources on the server). In case you do, you 305 | probably need to check the efficiency of your code/queries. (Feel free to 306 | reach out to us for assistance). 307 | 308 | my $module = $mcpan->all('releases', { scroller_time => '3m' }); 309 | 310 | =head2 scroller_size 311 | 312 | Note: please use with caution. 313 | 314 | This parameter will set the buffer size to be pulled from Elasticsearch 315 | when scrolling (default = 1000). 316 | This will affect query performance and memory usage, but you will still 317 | get an iterator back to fetch one object at a time. 318 | 319 | my $module = $mcpan->all('releases', { scroller_size => 500 }); 320 | 321 | =head3 sort 322 | 323 | Pass a raw Elasticsearch sort specification for the query. 324 | 325 | my $some_releases = $mcpan->all('releases', { sort => [{ date => { order => 'desc' } }] }) 326 | 327 | Note: this param and is a bit too specific to Elasticsearch. Just like 328 | L, only use this if you know what you're dealing with. 329 | 330 | =head1 SEARCH SPEC 331 | 332 | The hash-based search spec is common to many searches. It is quite 333 | feature-rich and allows you to disambiguate different types of searches. 334 | 335 | Basic search specs just contain a hash of keys and values: 336 | 337 | my $author = $mcpan->author( { name => 'Micha Nasriachi' } ); 338 | 339 | # the following is the same as ->author('MICKEY') 340 | my $author = $mcpan->author( { pauseid => 'MICKEY' } ); 341 | 342 | # find all people named Dave, not covering Davids 343 | # will return a resultset 344 | my $daves = $mcpan->author( { name => 'Dave *' } ); 345 | 346 | =head2 OR 347 | 348 | If you want to do a more complicated query that has an I condition, 349 | such as "this or that", you can use the following syntax with the C 350 | key: 351 | 352 | # any author named "Dave" or "David" 353 | my $daves = $mcpan->author( { 354 | either => [ 355 | { name => 'Dave *' }, 356 | { name => 'David *' }, 357 | ] 358 | } ); 359 | 360 | =head2 AND 361 | 362 | If you want to do a more complicated query that has an I condition, 363 | such as "this and that", you can use the following syntax with the C 364 | key: 365 | 366 | # any users named 'John' with a Gmail account 367 | my $johns = $mcpan->author( { 368 | all => [ 369 | { name => 'John *' }, 370 | { email => '*gmail.com' }, 371 | ] 372 | } ); 373 | 374 | Or, to get either the given version of a release, or the latest: 375 | 376 | my $releases = $mcpan->release( { 377 | all => [ 378 | { distribution => 'GraphViz2' }, 379 | ($version ? { version => $version } : { status => 'latest' }), 380 | ], 381 | } ); 382 | 383 | If you want to do something even more complicated, 384 | You can also nest your queries, e.g.: 385 | 386 | my $gmail_daves_or_cpan_sams = $mcpan->author( { 387 | either => [ 388 | { all => [ { name => 'Dave *' }, 389 | { email => '*gmail.com' } ] 390 | }, 391 | { all => [ { name => 'Sam *' }, 392 | { email => '*cpan.org' } ] 393 | }, 394 | ], 395 | } ); 396 | 397 | =head2 NOT 398 | 399 | If you want to filter out some of the results of an either/all query 400 | adding a I filter condition, such as "not these", you can use the 401 | following syntax with the C key: 402 | 403 | # any author named "Dave" or "David" 404 | my $daves = $mcpan->author( { 405 | either => [ 406 | { name => 'Dave *' }, 407 | { name => 'David *' }, 408 | ], 409 | not => [ 410 | { email => '*gmail.com' }, 411 | ], 412 | } ); 413 | 414 | =head1 DESIGN 415 | 416 | This module has three purposes: 417 | 418 | =over 4 419 | 420 | =item * Provide 100% of the MetaCPAN API 421 | 422 | This module will be updated regularly on every MetaCPAN API change, and intends 423 | to provide the user with as much of the API as possible, no shortcuts. If it's 424 | documented in the API, you should be able to do it. 425 | 426 | Because of this design decision, this module has an official MetaCPAN namespace 427 | with the blessing of the MetaCPAN developers. 428 | 429 | Notice this module currently only provides the beta API, not the old 430 | soon-to-be-deprecated API. 431 | 432 | =item * Be lightweight, to allow flexible usage 433 | 434 | While many modules would help make writing easier, it's important to take into 435 | account how they affect your compile-time, run-time, overall memory 436 | consumption, and CPU usage. 437 | 438 | By providing a slim interface implementation, more users are able to use this 439 | module, such as long-running processes (like daemons), CLI or GUI applications, 440 | cron jobs, and more. 441 | 442 | =item * DWIM 443 | 444 | While it's possible to access the methods defined by the API spec, there's still 445 | a matter of what you're really trying to achieve. For example, when searching 446 | for I<"Dave">, you want to find both I and I (and any 447 | other I), but you also want to search for a PAUSE ID of I, if one 448 | exists. 449 | 450 | This is where DWIM comes in. This module provides you with additional generic 451 | methods which will try to do what they think you want. 452 | 453 | Of course, this does not prevent you from manually using the API methods. You 454 | still have full control over that, if that's what you wish. 455 | 456 | You can (and should) read up on the general methods, which will explain how 457 | their DWIMish nature works, and what searches they run. 458 | 459 | =back 460 | 461 | =head1 AUTHORS 462 | 463 | =over 4 464 | 465 | =item * 466 | 467 | Sawyer X 468 | 469 | =item * 470 | 471 | Mickey Nasriachi 472 | 473 | =back 474 | 475 | =head1 COPYRIGHT AND LICENSE 476 | 477 | This software is copyright (c) 2016 by Sawyer X. 478 | 479 | This is free software; you can redistribute it and/or modify it under 480 | the same terms as the Perl 5 programming language system itself. 481 | 482 | =cut 483 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | requires "Carp" => "0"; 5 | requires "HTTP::Tiny" => "0.056"; 6 | requires "IO::Socket::SSL" => "1.42"; 7 | requires "JSON::MaybeXS" => "0"; 8 | requires "JSON::PP" => "0"; 9 | requires "Moo" => "0"; 10 | requires "Moo::Role" => "0"; 11 | requires "Net::SSLeay" => "1.49"; 12 | requires "Ref::Util" => "0"; 13 | requires "Safe::Isa" => "0"; 14 | requires "Type::Tiny" => "0"; 15 | requires "URI::Escape"; 16 | requires "perl" => "5.010"; 17 | requires "strict" => "0"; 18 | requires "warnings" => "0"; 19 | 20 | on 'test' => sub { 21 | requires "Test::Fatal" => "0"; 22 | requires "Test::More" => "0"; 23 | requires "Test::Needs" => "0.002005"; 24 | requires "base" => "0"; 25 | requires "blib" => "1.01"; 26 | requires "LWP::Protocol::https" => "0"; 27 | recommends "HTTP::Tiny::Mech" => "1.001002"; 28 | recommends "WWW::Mechanize::Cached" => "1.54"; 29 | }; 30 | 31 | on 'develop' => sub { 32 | requires "HTTP::Tiny::Mech" => "1.001002"; 33 | requires "LWP::Protocol::https" => "0"; 34 | requires "WWW::Mechanize::Cached" => "1.54"; 35 | }; 36 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = MetaCPAN-Client 2 | author = Sawyer X 3 | author = Mickey Nasriachi 4 | license = Perl_5 5 | copyright_holder = Sawyer X 6 | copyright_year = 2016 7 | 8 | version = 2.033000 9 | 10 | [@Starter] 11 | -remove = GatherDir 12 | MakeMaker.eumm_version = 7.1101 13 | 14 | [PodCoverageTests] 15 | 16 | [Git::GatherDir] 17 | 18 | [Prereqs::FromCPANfile] 19 | 20 | [PodWeaver] 21 | [MinimumPerlFast] 22 | 23 | [ReadmeAnyFromPod / pod.root] 24 | filename = README.pod 25 | type = pod 26 | location = root 27 | 28 | [CheckChangeLog] 29 | [PkgVersion] 30 | [MetaResources] 31 | bugtracker.web = https://github.com/metacpan/metacpan-client/issues 32 | repository.url = https://github.com/metacpan/metacpan-client.git 33 | repository.web = https://github.com/metacpan/metacpan-client 34 | repository.type = git 35 | x_IRC = irc://irc.perl.org/#metacpan 36 | x_WebIRC = https://chat.mibbit.com/#metacpan@irc.perl.org 37 | 38 | [Git::Tag] 39 | [Git::Push] 40 | -------------------------------------------------------------------------------- /examples/agg.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/agg.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $author = 10 | MetaCPAN::Client->new()->all( 11 | 'authors', 12 | { 13 | aggregations => { 14 | aggs => { 15 | terms => { 16 | field => "country" 17 | } 18 | } 19 | } 20 | } 21 | ); 22 | 23 | p $author->aggregations; 24 | -------------------------------------------------------------------------------- /examples/author-country.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/author-country.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my @countries = qw; 11 | 12 | my %cc2authors; 13 | 14 | for my $cc ( @countries ) { 15 | my $authors = 16 | MetaCPAN::Client->new->author({ country => $cc }); 17 | 18 | $cc2authors{$cc} = $authors->total; 19 | } 20 | 21 | p %cc2authors; 22 | -------------------------------------------------------------------------------- /examples/author.pl: -------------------------------------------------------------------------------- 1 | 2 | 3 | # examples/author.pl 4 | 5 | use strict; 6 | use warnings; 7 | use Data::Printer; 8 | use MetaCPAN::Client; 9 | 10 | my $author = 11 | MetaCPAN::Client->new()->author('XSAWYERX'); 12 | 13 | my %output = ( 14 | NAME => $author->name, 15 | EMAILS => $author->email, 16 | COUNTRY => $author->country, 17 | CITY => $author->city, 18 | PROFILE => $author->profile, 19 | LINKS => $author->links, 20 | RELEASE_COUNTS => $author->release_count, 21 | ); 22 | 23 | p %output; 24 | -------------------------------------------------------------------------------- /examples/author_releases.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Data::Printer; 4 | use MetaCPAN::Client; 5 | 6 | my $mcpan = MetaCPAN::Client->new; 7 | my $author = $mcpan->author('XSAWYERX'); 8 | my $releases = $author->releases; 9 | 10 | p $releases->total; 11 | 12 | while ( my $rel = $releases->next ) { 13 | p $rel->name; 14 | } 15 | -------------------------------------------------------------------------------- /examples/authors_blogs.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use MetaCPAN::Client; 5 | use Ref::Util qw< is_arrayref >; 6 | 7 | my $mcpan = MetaCPAN::Client->new; 8 | 9 | my $all_authors = $mcpan->all('authors'); 10 | 11 | AUTHOR: while ( my $author = $all_authors->next ) { 12 | 13 | BLOG: for my $blog ( @{ $author->blog || [] } ) { 14 | $blog and exists $blog->{url} or next BLOG; 15 | my $url = $blog->{url}; 16 | 17 | my $blogs_csv = is_arrayref($url) 18 | ? join q{,} => @$url 19 | : $url; 20 | 21 | printf "%-10s: %s\n", $author->pauseid, $blogs_csv; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/autocomplete.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/autocomplete.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $mcpan = MetaCPAN::Client->new(); 10 | 11 | my $ac = $mcpan->autocomplete("Danc"); 12 | 13 | p $ac; 14 | -------------------------------------------------------------------------------- /examples/autocomplete_suggest.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/autocomplete.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $mcpan = MetaCPAN::Client->new(); 10 | 11 | my $ac = $mcpan->autocomplete_suggest("Moos"); 12 | 13 | p $ac; 14 | -------------------------------------------------------------------------------- /examples/changes.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use MetaCPAN::Client; 4 | 5 | my $dist = shift || 'Moose'; 6 | 7 | my $mcpan = MetaCPAN::Client->new(); 8 | my $rel = $mcpan->release($dist); 9 | print $rel->changes; 10 | -------------------------------------------------------------------------------- /examples/complex-either-and.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/complex-either-and.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $authors = 10 | MetaCPAN::Client->new->author({ 11 | either => [ 12 | { name => 'Dave *' }, 13 | { name => 'David *' }, 14 | ], 15 | all => { email => '*gmail.com' }, 16 | }); 17 | 18 | my %output = ( 19 | TOTAL => $authors->total, 20 | NAMES => [ map { $authors->next->name } 0 .. 9 ], 21 | ); 22 | 23 | p %output; 24 | -------------------------------------------------------------------------------- /examples/complex-either-not.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/complex-either-not.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $authors = 10 | MetaCPAN::Client->new->author({ 11 | either => [ 12 | { name => 'Dave *' }, 13 | { name => 'David *' }, 14 | ], 15 | not => [ 16 | { name => 'Dave C*' }, 17 | { name => 'David M*' }, 18 | ] 19 | }); 20 | 21 | print "\n"; 22 | my %out = ( TOTAL => $authors->total ); 23 | p %out; 24 | -------------------------------------------------------------------------------- /examples/complex-nested-either-and.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/complex-nested-either-and.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $authors = 10 | MetaCPAN::Client->new->author({ 11 | either => [ 12 | { all => [ { name => 'Dave *' }, 13 | { email => '*gmail.com' } ] 14 | }, 15 | { all => [ { name => 'Sam *' }, 16 | { email => '*cpan.org' } ] 17 | }, 18 | ], 19 | }); 20 | 21 | my %output = ( 22 | TOTAL => $authors->total, 23 | NAMES => [ map { $authors->next->name } 0 .. 9 ], 24 | ); 25 | 26 | p %output; 27 | -------------------------------------------------------------------------------- /examples/complex.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/complex.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $authors = 10 | MetaCPAN::Client->new->author({ 11 | either => [ 12 | { name => 'Dave *' }, 13 | { name => 'David *' }, 14 | ] 15 | }); 16 | 17 | my %output = ( 18 | TOTAL => $authors->total, 19 | NAMES => [ map { $authors->next->name } 0 .. 9 ], 20 | ); 21 | 22 | p %output; 23 | -------------------------------------------------------------------------------- /examples/contributors.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use MetaCPAN::Client; 4 | use Data::Printer; 5 | 6 | my $mcpan = MetaCPAN::Client->new(); 7 | 8 | my $release = $mcpan->release({ 9 | all => [ 10 | { distribution => 'Moose' }, 11 | { version => '2.2005' }, 12 | ] 13 | })->next; 14 | 15 | my $contributors = $release->contributors; 16 | 17 | p $contributors; 18 | 19 | 1; 20 | -------------------------------------------------------------------------------- /examples/cover.pl: -------------------------------------------------------------------------------- 1 | # examples/cover.pl 2 | 3 | use strict; 4 | use warnings; 5 | use Data::Printer; 6 | use MetaCPAN::Client; 7 | 8 | my $mcpan = MetaCPAN::Client->new(); 9 | my $coverage = $mcpan->cover('Moose-2.2007'); 10 | p $coverage; 11 | 12 | 1; 13 | -------------------------------------------------------------------------------- /examples/distribution.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/distribution.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $mcpan = MetaCPAN::Client->new(); 10 | my $dist = $mcpan->distribution('Moose'); 11 | 12 | my %output = ( 13 | NAME => $dist->name, 14 | BUGS => $dist->bugs, 15 | RIVER => $dist->river, 16 | ); 17 | 18 | p %output; 19 | -------------------------------------------------------------------------------- /examples/download_url.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/download_url.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | 8 | use MetaCPAN::Client; 9 | 10 | my $mcpan = MetaCPAN::Client->new(); 11 | 12 | my $download_url = $mcpan->download_url('Moose'); 13 | 14 | my %output = ( 15 | VERSION => $download_url->version, 16 | STATUS => $download_url->status, 17 | DATE => $download_url->date, 18 | DOWNLOAD_URL => $download_url->download_url, 19 | ); 20 | 21 | p %output; 22 | -------------------------------------------------------------------------------- /examples/es_filter.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use MetaCPAN::Client; 5 | 6 | # use raw ES filter on a { match_all => {} } query 7 | 8 | # find 'latest' status releases with at least 700 failing tests 9 | # which consist at least 50% of their overall number of tests. 10 | 11 | my $release = MetaCPAN::Client->new->all( 12 | 'releases', 13 | { 14 | es_filter => { 15 | and => [ 16 | { range => { 'tests.fail' => { gte => 700 } } }, 17 | { term => { 'status' => 'latest' } } 18 | ] 19 | }, 20 | } 21 | ); 22 | 23 | while ( my $r = $release->next ) { 24 | my $fail = $r->tests->{fail}; 25 | my $all = 0; $all += $_ for @{ $r->tests }{qw/pass fail na unknown/}; 26 | ($fail / $all) >= 0.5 and printf "%4d/%4d: %s\n", $fail, $all, $r->name; 27 | } 28 | -------------------------------------------------------------------------------- /examples/fields-filter.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/fields-filter.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | 8 | use MetaCPAN::Client; 9 | 10 | my $module = 11 | MetaCPAN::Client->new->module('Moose', 12 | { fields => [ qw/ author version / ] }); 13 | 14 | p $module; 15 | -------------------------------------------------------------------------------- /examples/metacpan_url.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use MetaCPAN::Client; 4 | 5 | my $mcpan = MetaCPAN::Client->new(); 6 | 7 | my $auth = $mcpan->author('HAARG'); 8 | my $mod = $mcpan->module('Moo'); 9 | my $file = $mcpan->file('HAARG/Moo-2.003001/lib/Moo.pm'); 10 | my $dist = $mcpan->distribution('Moo'); 11 | my $rel = $mcpan->release({ 12 | all => [ 13 | { distribution => 'Moo' }, 14 | { version => '2.002005' }, 15 | ] 16 | }); 17 | 18 | printf "AUTHOR : %s\n", $auth->metacpan_url; 19 | printf "RELEASE : %s\n", $rel->next->metacpan_url; 20 | printf "MODULE : %s\n", $mod->metacpan_url; 21 | printf "FILE : %s\n", $file->metacpan_url; 22 | printf "DISTRIBUTION : %s\n", $dist->metacpan_url; 23 | 24 | 1; 25 | -------------------------------------------------------------------------------- /examples/mirror.pl: -------------------------------------------------------------------------------- 1 | 2 | 3 | # examples/mirror.pl 4 | 5 | use strict; 6 | use warnings; 7 | use Data::Printer; 8 | use MetaCPAN::Client; 9 | 10 | my $mirrors = 11 | MetaCPAN::Client->new->mirror('eutelia.it'); 12 | 13 | p $mirrors; 14 | 15 | -------------------------------------------------------------------------------- /examples/module.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/module.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | 8 | use MetaCPAN::Client; 9 | 10 | my $module = 11 | MetaCPAN::Client->new->module('Moo'); 12 | 13 | my %output = ( 14 | NAME => $module->name, 15 | ABSTRACT => $module->abstract, 16 | DESCRIPTION => $module->description, 17 | RELEASE => $module->release, 18 | AUTHOR => $module->author, 19 | VERSION => $module->version, 20 | ); 21 | 22 | p %output; 23 | -------------------------------------------------------------------------------- /examples/package.pl: -------------------------------------------------------------------------------- 1 | # examples/package.pl 2 | 3 | use strict; 4 | use warnings; 5 | use Data::Printer; 6 | use MetaCPAN::Client; 7 | 8 | my $mcpan = MetaCPAN::Client->new(); 9 | my $pack = $mcpan->package('MooseX::Types'); 10 | p $pack; 11 | 12 | 1; 13 | __END__ 14 | 15 | Alternatively: 16 | 17 | my $module = $mcpan->module('MooseX::Types'); 18 | my $pack = $module->package; 19 | -------------------------------------------------------------------------------- /examples/permission.pl: -------------------------------------------------------------------------------- 1 | # examples/permission.pl 2 | 3 | use strict; 4 | use warnings; 5 | use Data::Printer; 6 | use MetaCPAN::Client; 7 | 8 | my $mcpan = MetaCPAN::Client->new(); 9 | my $perm = $mcpan->permission('MooseX::Types'); 10 | p $perm; 11 | 12 | 1; 13 | __END__ 14 | 15 | Alternatively: 16 | 17 | my $module = $mcpan->module('MooseX::Types'); 18 | my $perm = $module->permission; 19 | -------------------------------------------------------------------------------- /examples/pod.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/pod.pl 3 | 4 | use strict; 5 | use warnings; 6 | use MetaCPAN::Client; 7 | 8 | my $pod = 9 | MetaCPAN::Client->new->pod('Moo'); 10 | 11 | print $pod->html; 12 | -------------------------------------------------------------------------------- /examples/rating.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/rating.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | 8 | use MetaCPAN::Client; 9 | 10 | my $rating = 11 | MetaCPAN::Client->new->rating({ distribution => "Moose" }); 12 | 13 | p $rating->next; 14 | -------------------------------------------------------------------------------- /examples/recent.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/recent.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $recent = 10 | MetaCPAN::Client->new->recent(3); 11 | 12 | while ( my $rel = $recent->next ) { 13 | my %output = ( 14 | NAME => $rel->name, 15 | AUTHOR => $rel->author, 16 | DATE => $rel->date, 17 | VERSION => $rel->version, 18 | ); 19 | 20 | p %output; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/recent_today.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Data::Printer; 4 | use MetaCPAN::Client; 5 | 6 | my $recent = 7 | MetaCPAN::Client->new->recent('today'); 8 | 9 | while ( my $rel = $recent->next ) { 10 | my %output = ( 11 | NAME => $rel->name, 12 | AUTHOR => $rel->author, 13 | DATE => $rel->date, 14 | VERSION => $rel->version, 15 | ); 16 | 17 | p %output; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /examples/release.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Data::Printer; 4 | use MetaCPAN::Client; 5 | 6 | my $release = 7 | MetaCPAN::Client->new->release('Devel-NYTProf'); 8 | 9 | my %output = ( 10 | AUTHOR => $release->author, 11 | DATE => $release->date, 12 | STATUS => $release->status, 13 | VERSION => $release->version, 14 | TESTS => $release->tests, 15 | ); 16 | 17 | p %output; 18 | -------------------------------------------------------------------------------- /examples/rev_deps-recursive.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/rev_deps-recursive.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Term::ANSIColor; 7 | use MetaCPAN::Client; 8 | 9 | $|=1; 10 | 11 | my $dist = shift || 'Hijk'; 12 | my $mcpan = MetaCPAN::Client->new; 13 | 14 | print "\n\n", colored( "* $dist", 'green' ), "\n"; 15 | dig( $dist, 0 ); 16 | 17 | sub dig { 18 | my $dist = shift; 19 | my $level = shift; 20 | 21 | my $res = $mcpan->reverse_dependencies($dist); 22 | 23 | while ( my $item = $res->next ) { 24 | if ( $level ) { 25 | printf "%s%s\n", 26 | colored( '....' x $level, 'yellow' ), 27 | $item->distribution; 28 | } else { 29 | printf "\n>> %s\n", 30 | colored( $item->distribution, 'blue' ); 31 | } 32 | 33 | dig( $item->distribution, $level + 1 ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/rev_deps.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/rev_deps.pl 3 | 4 | use strict; 5 | use warnings; 6 | use Data::Printer; 7 | use MetaCPAN::Client; 8 | 9 | my $module = shift || 'Hijk'; 10 | 11 | my $deps = 12 | MetaCPAN::Client->new->rev_deps($module); 13 | 14 | my @output; 15 | 16 | while ( my $rel = $deps->next ) { 17 | push @output => { 18 | name => $rel->name, 19 | author => $rel->author, 20 | }; 21 | } 22 | 23 | print "\n"; 24 | my $title = "Reverse dependencies for '$module':"; 25 | p $title; 26 | p @output; 27 | -------------------------------------------------------------------------------- /examples/top20_favorites.pl: -------------------------------------------------------------------------------- 1 | 2 | # examples/top20_favorites 3 | 4 | use strict; 5 | use warnings; 6 | use MetaCPAN::Client; 7 | 8 | my $top20_fav = 9 | MetaCPAN::Client->new()->all( 10 | 'favorites', 11 | { 12 | aggregations => { 13 | aggs => { 14 | terms => { 15 | field => "distribution", 16 | size => 20, 17 | } 18 | } 19 | } 20 | } 21 | ); 22 | 23 | for my $bucket ( @{ $top20_fav->aggregations->{aggs}{buckets} } ) { 24 | printf "%s : %s\n", $bucket->{doc_count}, $bucket->{key} 25 | } 26 | -------------------------------------------------------------------------------- /examples/totals.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Data::Printer; 4 | use MetaCPAN::Client; 5 | 6 | my $mcpan = MetaCPAN::Client->new; 7 | 8 | my $all_authors = $mcpan->all('authors'); 9 | my $all_dists = $mcpan->all('distributions'); 10 | my $all_modules = $mcpan->all('modules'); 11 | my $all_releases = $mcpan->all('releases'); 12 | 13 | print "totals:\n"; 14 | printf "authors : %d\n", $all_authors->total; 15 | printf "distributions : %d\n", $all_dists->total; 16 | printf "modules : %d\n", $all_modules->total; 17 | printf "releases : %d\n", $all_releases->total; 18 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client; 4 | # ABSTRACT: A comprehensive, DWIM-featured client to the MetaCPAN API 5 | 6 | use Moo; 7 | use Carp; 8 | use Ref::Util qw< is_arrayref is_hashref is_ref >; 9 | use URI::Escape qw< uri_escape_utf8 >; 10 | 11 | use MetaCPAN::Client::Request; 12 | use MetaCPAN::Client::Author; 13 | use MetaCPAN::Client::Distribution; 14 | use MetaCPAN::Client::DownloadURL; 15 | use MetaCPAN::Client::Module; 16 | use MetaCPAN::Client::File; 17 | use MetaCPAN::Client::Favorite; 18 | use MetaCPAN::Client::Pod; 19 | use MetaCPAN::Client::Rating; 20 | use MetaCPAN::Client::Release; 21 | use MetaCPAN::Client::Mirror; 22 | use MetaCPAN::Client::Package; 23 | use MetaCPAN::Client::Permission; 24 | use MetaCPAN::Client::ResultSet; 25 | use MetaCPAN::Client::Cover; 26 | 27 | has request => ( 28 | is => 'ro', 29 | handles => [qw], 30 | ); 31 | 32 | my @supported_searches = qw< 33 | author distribution favorite module rating release mirror file permission package cover 34 | >; 35 | 36 | sub BUILDARGS { 37 | my ( $class, %args ) = @_; 38 | 39 | $args{'request'} ||= MetaCPAN::Client::Request->new( 40 | ( ua => $args{ua} )x!! $args{ua}, 41 | ( ua_args => $args{ua_args} )x!! $args{ua_args}, 42 | ( domain => $args{domain} )x!! $args{domain}, 43 | ( debug => $args{debug} )x!! $args{debug}, 44 | ); 45 | 46 | return \%args; 47 | } 48 | 49 | sub author { 50 | my $self = shift; 51 | my $arg = shift; 52 | my $params = shift; 53 | 54 | return $self->_get_or_search( 'author', $arg, $params ); 55 | } 56 | 57 | sub module { 58 | my $self = shift; 59 | my $arg = shift; 60 | my $params = shift; 61 | 62 | return $self->_get_or_search( 'module', $arg, $params ); 63 | } 64 | 65 | sub distribution { 66 | my $self = shift; 67 | my $arg = shift; 68 | my $params = shift; 69 | 70 | return $self->_get_or_search( 'distribution', $arg, $params ); 71 | } 72 | 73 | sub file { 74 | my $self = shift; 75 | my $arg = shift; 76 | my $params = shift; 77 | 78 | return $self->_get_or_search( 'file', $arg, $params ); 79 | } 80 | 81 | sub package { 82 | my $self = shift; 83 | my $arg = shift; 84 | my $params = shift; 85 | 86 | return $self->_get_or_search( 'package', $arg, $params ); 87 | } 88 | 89 | sub permission { 90 | my $self = shift; 91 | my $arg = shift; 92 | my $params = shift; 93 | 94 | return $self->_get_or_search( 'permission', $arg, $params ); 95 | } 96 | 97 | sub cover { 98 | my $self = shift; 99 | my $arg = shift; 100 | my $params = shift; 101 | 102 | return $self->_get_or_search( 'cover', $arg, $params ); 103 | } 104 | 105 | sub pod { 106 | my $self = shift; 107 | my $name = shift; 108 | my $params = shift || {}; 109 | 110 | return MetaCPAN::Client::Pod->new({ 111 | request => $self->request, 112 | name => $name, 113 | %$params 114 | }); 115 | } 116 | 117 | sub favorite { 118 | my $self = shift; 119 | my $args = shift; 120 | my $params = shift; 121 | 122 | is_hashref($args) 123 | or croak 'favorite takes a hash ref as parameter'; 124 | 125 | return $self->_search( 'favorite', $args, $params ); 126 | } 127 | 128 | sub rating { 129 | my $self = shift; 130 | my $args = shift; 131 | my $params = shift; 132 | 133 | is_hashref($args) 134 | or croak 'rating takes a hash ref as parameter'; 135 | 136 | return _empty_result_set('rating'); 137 | } 138 | 139 | sub release { 140 | my $self = shift; 141 | my $arg = shift; 142 | my $params = shift; 143 | 144 | return $self->_get_or_search( 'release', $arg, $params ); 145 | } 146 | 147 | sub mirror { 148 | my $self = shift; 149 | my $arg = shift; 150 | my $params = shift; 151 | 152 | return $self->_get_or_search( 'mirror', $arg, $params ); 153 | } 154 | 155 | sub reverse_dependencies { 156 | my $self = shift; 157 | my $dist = shift; 158 | 159 | $dist =~ s/::/-/g; 160 | 161 | return $self->_reverse_deps($dist); 162 | } 163 | 164 | *rev_deps = *reverse_dependencies; 165 | 166 | sub recent { 167 | my $self = shift; 168 | my $size = shift || 100; 169 | 170 | $size eq 'today' 171 | and return $self->_recent( 172 | size => 1000, 173 | filter => _filter_today() 174 | ); 175 | 176 | $size =~ /^[0-9]+$/ 177 | and return $self->_recent( size => $size ); 178 | 179 | croak "recent: invalid size value"; 180 | } 181 | 182 | sub all { 183 | my $self = shift; 184 | my $type = shift; 185 | my $params = shift; 186 | 187 | # This endpoint used to support only pluralized types (mostly) and convert 188 | # to singular types before redispatching. Now it accepts both plural and 189 | # unplural forms directly and relies on the underlying methods it 190 | # dispatches to to check types (using the global supported types array). 191 | $type =~ s/s$//; 192 | 193 | $params and !is_hashref($params) 194 | and croak "all: params must be a hashref"; 195 | 196 | if ( $params->{fields} and !is_arrayref($params->{fields}) ) { 197 | $params->{fields} = [ split /,/ => $params->{fields} ]; 198 | } 199 | 200 | return $self->$type( { __MATCH_ALL__ => 1 }, $params ); 201 | } 202 | 203 | sub download_url { 204 | my $self = shift; 205 | my $module = shift; 206 | my $version_or_range = shift; 207 | my $dev = shift; 208 | 209 | my $uri = $module; 210 | my @extra; 211 | if ( defined $version_or_range ) { 212 | 213 | my @valid_ranges = qw{ == != <= >= < > ! }; 214 | my $is_using_range; 215 | foreach my $range ( @valid_ranges ) { 216 | if ( index( $version_or_range, $range ) >= 0 ) { 217 | $is_using_range = 1; 218 | last; 219 | } 220 | } 221 | # by default use the '==' operator when no range set 222 | $version_or_range = '==' . $version_or_range unless $is_using_range; 223 | 224 | # version=>0.21,<0.27,!=0.26&dev=1 225 | push @extra, 'version=' .uri_escape_utf8($version_or_range); 226 | } 227 | if ( defined $dev ) { 228 | push @extra, 'dev=' . uri_escape_utf8($dev); 229 | } 230 | 231 | $uri .= '?'.join('&', @extra) if scalar @extra; 232 | 233 | return $self->_get( 'download_url', $uri ); 234 | } 235 | 236 | sub autocomplete { 237 | my $self = shift; 238 | my $q = shift; 239 | 240 | my $res; 241 | 242 | eval { 243 | $res = $self->fetch( '/search/autocomplete?q=' . uri_escape_utf8($q) ); 244 | 1; 245 | 246 | } or do { 247 | warn $@; 248 | return []; 249 | }; 250 | 251 | return [ 252 | map { $_->{fields} } @{ $res->{hits}{hits} } 253 | ]; 254 | } 255 | 256 | sub autocomplete_suggest { 257 | my $self = shift; 258 | my $q = shift; 259 | 260 | my $res; 261 | 262 | eval { 263 | $res = $self->fetch( '/search/autocomplete/suggest?q=' . uri_escape_utf8($q) ); 264 | 1; 265 | 266 | } or do { 267 | warn $@; 268 | return []; 269 | }; 270 | 271 | return $res->{suggestions}; 272 | } 273 | 274 | ### 275 | 276 | sub _get { 277 | my $self = shift; 278 | 279 | ( scalar(@_) == 2 280 | or ( scalar(@_) == 3 and ( !defined $_[2] or is_hashref($_[2]) ) ) ) 281 | or croak '_get takes type and search string as parameters (and an optional params hash)'; 282 | 283 | my $type = shift; 284 | my $arg = shift; 285 | my $params = shift; 286 | 287 | my $fields_filter = $self->_read_fields( $params ); 288 | 289 | my $response = $self->fetch( 290 | sprintf("%s/%s%s", $type ,$arg, $fields_filter||'') 291 | ); 292 | is_hashref($response) 293 | or croak sprintf( 'Failed to fetch %s (%s)', ucfirst($type), $arg ); 294 | 295 | $type = 'DownloadURL' if $type eq 'download_url'; 296 | 297 | my $class = 'MetaCPAN::Client::' . ucfirst($type); 298 | return $class->new_from_request($response, $self); 299 | } 300 | 301 | sub _read_fields { 302 | my $self = shift; 303 | my $params = shift; 304 | $params or return; 305 | 306 | my $fields = delete $params->{fields}; 307 | $fields or return; 308 | 309 | if ( is_arrayref($fields) ) { 310 | grep { ref $_ } @$fields 311 | and croak "fields array should not contain any refs."; 312 | 313 | return sprintf( "?fields=%s", join q{,} => @$fields ); 314 | 315 | } elsif ( !ref $fields ) { 316 | 317 | return "?fields=$fields"; 318 | } 319 | 320 | croak "invalid param: fields"; 321 | } 322 | 323 | sub _search { 324 | my $self = shift; 325 | my $type = shift; 326 | my $args = shift; 327 | my $params = shift; 328 | 329 | is_hashref($args) 330 | or croak '_search takes a hash ref as query'; 331 | 332 | ! defined $params or is_hashref($params) 333 | or croak '_search takes a hash ref as query parameters'; 334 | 335 | $params ||= {}; 336 | 337 | grep { $_ eq $type } @supported_searches 338 | or croak 'search type is not supported'; 339 | 340 | my $scroller = $self->ssearch($type, $args, $params); 341 | 342 | return MetaCPAN::Client::ResultSet->new( 343 | scroller => $scroller, 344 | type => $type, 345 | ); 346 | } 347 | 348 | sub _get_or_search { 349 | my $self = shift; 350 | my $type = shift; 351 | my $arg = shift; 352 | my $params = shift; 353 | 354 | is_hashref($arg) and 355 | return $self->_search( $type, $arg, $params ); 356 | 357 | defined $arg and !is_ref($arg) 358 | and return $self->_get($type, $arg, $params); 359 | 360 | croak "$type: invalid args (takes scalar value or search parameters hashref)"; 361 | } 362 | 363 | sub _reverse_deps { 364 | my $self = shift; 365 | my $dist = shift; 366 | 367 | my $res; 368 | 369 | eval { 370 | $res = $self->fetch( 371 | "/reverse_dependencies/dist/$dist", 372 | { 373 | size => 5000, 374 | query => { match_all => {} }, 375 | filter => { 376 | and => [ 377 | { term => { 'status' => 'latest' } }, 378 | { term => { 'authorized' => 1 } }, 379 | ] 380 | }, 381 | } 382 | ); 383 | 1; 384 | 385 | } or do { 386 | warn $@; 387 | return _empty_result_set('release'), 388 | }; 389 | 390 | return MetaCPAN::Client::ResultSet->new( 391 | items => $res->{'data'}, 392 | type => 'release', 393 | ); 394 | } 395 | 396 | sub _recent { 397 | my $self = shift; 398 | my @args = @_; 399 | 400 | my $res; 401 | 402 | eval { 403 | $res = $self->fetch( 404 | '/release/_search', 405 | { 406 | from => 0, 407 | query => { match_all => {} }, 408 | @args, 409 | sort => [ { 'date' => { order => "desc" } } ], 410 | } 411 | ); 412 | 1; 413 | 414 | } or do { 415 | warn $@; 416 | return _empty_result_set('release'); 417 | }; 418 | 419 | return MetaCPAN::Client::ResultSet->new( 420 | items => $res->{'hits'}{'hits'}, 421 | type => 'release', 422 | ); 423 | } 424 | 425 | sub _filter_today { 426 | return { range => { date => { from => "now/1d+0h" } } }; 427 | } 428 | 429 | sub _empty_result_set { 430 | my $type = shift; 431 | 432 | return MetaCPAN::Client::ResultSet->new( 433 | items => [], 434 | type => $type, 435 | ); 436 | } 437 | 438 | 1; 439 | 440 | __END__ 441 | 442 | =head1 SYNOPSIS 443 | 444 | # simple usage 445 | my $mcpan = MetaCPAN::Client->new(); 446 | my $author = $mcpan->author('XSAWYERX'); 447 | my $dist = $mcpan->distribution('MetaCPAN-Client'); 448 | 449 | # advanced usage with cache (contributed by Kent Fredric) 450 | use CHI; 451 | use WWW::Mechanize::Cached; 452 | use HTTP::Tiny::Mech; 453 | use MetaCPAN::Client; 454 | 455 | my $mcpan = MetaCPAN::Client->new( 456 | ua => HTTP::Tiny::Mech->new( 457 | mechua => WWW::Mechanize::Cached->new( 458 | cache => CHI->new( 459 | driver => 'File', 460 | root_dir => '/tmp/metacpan-cache', 461 | ), 462 | ), 463 | ), 464 | ); 465 | 466 | # now $mcpan caches results 467 | 468 | =head1 DESCRIPTION 469 | 470 | This is a hopefully-complete API-compliant client to MetaCPAN 471 | (L) with DWIM capabilities, to make your life easier. 472 | 473 | =head1 ATTRIBUTES 474 | 475 | =head2 request 476 | 477 | Internal attribute representing the request object making the request to 478 | MetaCPAN and analyzing the results. You probably don't want to set this, nor 479 | should you have any usage of it. 480 | 481 | =head2 ua 482 | 483 | If provided, L will use the user agent object 484 | instead of the default, which is L. 485 | 486 | Then it can be used to fetch the user agent object used by 487 | L. 488 | 489 | =head2 domain 490 | 491 | If given, will be used to alter the API domain. 492 | 493 | =head2 debug 494 | 495 | If given, errors will include some low-level detailed message. 496 | 497 | =head1 METHODS 498 | 499 | =head2 author 500 | 501 | my $author = $mcpan->author('XSAWYERX'); 502 | my $author = $mcpan->author($search_spec); 503 | 504 | Finds an author by either its PAUSE ID or by a search spec defined by a hash 505 | reference. Since it is common to many other searches, it is explained below 506 | under C. 507 | 508 | Returns a L object on a simple search (PAUSE ID), or 509 | a L object populated with 510 | L objects on a complex (L) search. 511 | 512 | =head2 cover 513 | 514 | my $cover = $mcpan->cover('Moose-2.2007'); 515 | 516 | Returns a L object. 517 | 518 | =head2 distribution 519 | 520 | my $dist = $mcpan->distribution('MetaCPAN-Client'); 521 | my $dist = $mcpan->distribution($search_spec); 522 | 523 | Finds a distribution by either its distribution name or by a search spec 524 | defined by a hash reference. Since it is common to many other searches, it is 525 | explained below under C. 526 | 527 | Returns a L object on a simple search 528 | (distribution name), or a L object populated with 529 | L objects on a complex (L) 530 | search. 531 | 532 | =head2 file 533 | 534 | Returns a L object. 535 | 536 | =head2 favorite 537 | 538 | my $favorite = $mcpan->favorite({ distribution => 'Moose' }); 539 | 540 | Returns a L object containing 541 | L results. 542 | 543 | =head2 rating 544 | 545 | my $rating = $mcpan->rating({ distribution => 'Moose' }); 546 | 547 | Returns a L object containing 548 | L results. 549 | 550 | =head2 release 551 | 552 | my $release = $mcpan->release('MetaCPAN-Client'); 553 | my $release = $mcpan->release($search_spec); 554 | 555 | Finds a release by either its distribution name or by a search spec defined by 556 | a hash reference. Since it is common to many other searches, it is explained 557 | below under C. 558 | 559 | Returns a L object on a simple search (release name), 560 | or a L object populated with 561 | L objects on a complex (L) search. 562 | 563 | =head2 mirror 564 | 565 | my $mirror = $mcpan->mirror('kr.freebsd.org'); 566 | 567 | Returns a L object. 568 | 569 | =head2 module 570 | 571 | my $module = $mcpan->module('MetaCPAN::Client'); 572 | my $module = $mcpan->module($search_spec); 573 | 574 | Finds a module by either its module name or by a search spec defined by a hash 575 | reference. Since it is common to many other searches, it is explained below 576 | under C. 577 | 578 | Returns a L object on a simple search (module name), or 579 | a L object populated with 580 | L objects on a complex (L) search. 581 | 582 | =head2 package 583 | 584 | my $package = $mcpan->package('MooseX::Types'); 585 | 586 | Returns a L object. 587 | 588 | =head2 permission 589 | 590 | my $permission = $mcpan->permission('MooseX::Types'); 591 | 592 | Returns a L object. 593 | 594 | =head2 reverse_dependencies 595 | 596 | my $deps = $mcpan->reverse_dependencies('Search::Elasticsearch'); 597 | 598 | all L objects of releases that are directly 599 | dependent on a given module, returned as L. 600 | 601 | =head2 rev_deps 602 | 603 | Alias to C described above. 604 | 605 | =head2 autocomplete 606 | 607 | my $ac = $mcpan->autocomplete('Danc'); 608 | 609 | Call the search/autocomplete endpoint with a query string. 610 | 611 | Returns an array reference. 612 | 613 | =head2 autocomplete_suggest 614 | 615 | my $ac = $mcpan->autocomplete_suggest('Moo'); 616 | 617 | Call the search/autocomplete/suggest endpoint with a query string. 618 | 619 | Returns an array reference. 620 | 621 | =head2 recent 622 | 623 | my $recent = $mcpan->recent(10); 624 | my $recent = $mcpan->recent('today'); 625 | 626 | return the latest N releases, or all releases from today. 627 | 628 | returns a L of L. 629 | 630 | =head2 pod 631 | 632 | Get POD for given file/module name. 633 | returns a L object, which supports various output 634 | formats (html, plain, x_pod & x_markdown). 635 | 636 | my $pod = $mcpan->pod('Moo')->html; 637 | my $pod = $mcpan->pod('Moo', { url_prefix => $prefix })->html; 638 | 639 | =head2 download_url 640 | 641 | Retrieve information from the 'download_url' endpoint 642 | 643 | my $download_url = $mcpan->download_url($distro, [$version_or_range, $dev]); 644 | 645 | # request the last available version 646 | my $download_url = $mcpan->download_url('Moose'); 647 | 648 | # request an older version 649 | my $download_url = $mcpan->download_url('Moose', '1.01'); 650 | 651 | # using a range 652 | my $download_url = $mcpan->download_url('Moose', '<=1.01'); 653 | my $download_url = $mcpan->download_url('Moose', '>1.01,<=2.00'); 654 | 655 | Range operators are '== != <= >= < > !'. 656 | You can use a comma ',' to add multiple rules. 657 | 658 | # requesting dev release 659 | my $download_url = $mcpan->download_url('Moose', '>1.01', 1); 660 | 661 | 662 | Returns a L object 663 | 664 | =head2 all 665 | 666 | Retrieve all matches for authors/modules/distributions/favorites or releases. 667 | 668 | my $all_releases = $mcpan->all('releases') 669 | 670 | When called with a second parameter containing a hash ref, 671 | will support the following keys: 672 | 673 | =head3 fields 674 | 675 | See SEARCH PARAMS. 676 | 677 | my $all_releases = $mcpan->all('releases', { fields => [...] }) 678 | 679 | =head3 _source 680 | 681 | See SEARCH PARAMS. 682 | 683 | my $all_releases = $mcpan->all('releases', { _source => [...] }) 684 | 685 | =head3 es_filter 686 | 687 | Pass a raw Elasticsearch filter structure to reduce the number 688 | of elements returned by the query. 689 | 690 | my $some_releases = $mcpan->all('releases', { es_filter => {...} }) 691 | 692 | =head2 BUILDARGS 693 | 694 | Internal construction wrapper. Do not use. 695 | 696 | =head1 SEARCH PARAMS 697 | 698 | Most searches take params as an optional hash-ref argument. 699 | these params will be passed to the search action. 700 | 701 | In non-scrolled searches, 'fields' filter is the only supported 702 | parameter ATM. 703 | 704 | =head2 fields 705 | 706 | Filter the fields to reduce the amount of data pulled from MetaCPAN. 707 | can be passed as a csv list or an array ref. 708 | 709 | my $module = $mcpan->module('Moose', { fields => "version,author" }); 710 | my $module = $mcpan->module('Moose', { fields => [qw/version author/] }); 711 | 712 | =head2 _source 713 | 714 | Note: this param and its description are a bit too Elasticsearch specific. 715 | just like 'es_filter' - use only if you know what you're dealing with. 716 | 717 | Some fields are not indexed in Elasticsearch but stored as part of 718 | the entire document. 719 | 720 | These fields can still be read, but without the internal Elasticsearch 721 | optimizations and the server will internally read the whole document. 722 | 723 | Why do we even need those? because we don't index everything and some things 724 | we can't to begin with (like non-leaf fields that hold a structure) 725 | 726 | my $module = $mcpan->all('releases', { _source => "stat" }); 727 | 728 | =head2 scroller_time 729 | 730 | Note: please use with caution. 731 | 732 | This parameter will set the maximum lifetime of the Elasticsearch scroller on 733 | the server (default = '5m'). Normally you do not need to set this value (as 734 | tweaking this value can affect resources on the server). In case you do, you 735 | probably need to check the efficiency of your code/queries. (Feel free to 736 | reach out to us for assistance). 737 | 738 | my $module = $mcpan->all('releases', { scroller_time => '3m' }); 739 | 740 | =head2 scroller_size 741 | 742 | Note: please use with caution. 743 | 744 | This parameter will set the buffer size to be pulled from Elasticsearch 745 | when scrolling (default = 1000). 746 | This will affect query performance and memory usage, but you will still 747 | get an iterator back to fetch one object at a time. 748 | 749 | my $module = $mcpan->all('releases', { scroller_size => 500 }); 750 | 751 | =head3 sort 752 | 753 | Pass a raw Elasticsearch sort specification for the query. 754 | 755 | my $some_releases = $mcpan->all('releases', { sort => [{ date => { order => 'desc' } }] }) 756 | 757 | Note: this param and is a bit too specific to Elasticsearch. Just like 758 | L, only use this if you know what you're dealing with. 759 | 760 | =head1 SEARCH SPEC 761 | 762 | The hash-based search spec is common to many searches. It is quite 763 | feature-rich and allows you to disambiguate different types of searches. 764 | 765 | Basic search specs just contain a hash of keys and values: 766 | 767 | my $author = $mcpan->author( { name => 'Micha Nasriachi' } ); 768 | 769 | # the following is the same as ->author('MICKEY') 770 | my $author = $mcpan->author( { pauseid => 'MICKEY' } ); 771 | 772 | # find all people named Dave, not covering Davids 773 | # will return a resultset 774 | my $daves = $mcpan->author( { name => 'Dave *' } ); 775 | 776 | =head2 OR 777 | 778 | If you want to do a more complicated query that has an I condition, 779 | such as "this or that", you can use the following syntax with the C 780 | key: 781 | 782 | # any author named "Dave" or "David" 783 | my $daves = $mcpan->author( { 784 | either => [ 785 | { name => 'Dave *' }, 786 | { name => 'David *' }, 787 | ] 788 | } ); 789 | 790 | =head2 AND 791 | 792 | If you want to do a more complicated query that has an I condition, 793 | such as "this and that", you can use the following syntax with the C 794 | key: 795 | 796 | # any users named 'John' with a Gmail account 797 | my $johns = $mcpan->author( { 798 | all => [ 799 | { name => 'John *' }, 800 | { email => '*gmail.com' }, 801 | ] 802 | } ); 803 | 804 | Or, to get either the given version of a release, or the latest: 805 | 806 | my $releases = $mcpan->release( { 807 | all => [ 808 | { distribution => 'GraphViz2' }, 809 | ($version ? { version => $version } : { status => 'latest' }), 810 | ], 811 | } ); 812 | 813 | If you want to do something even more complicated, 814 | You can also nest your queries, e.g.: 815 | 816 | my $gmail_daves_or_cpan_sams = $mcpan->author( { 817 | either => [ 818 | { all => [ { name => 'Dave *' }, 819 | { email => '*gmail.com' } ] 820 | }, 821 | { all => [ { name => 'Sam *' }, 822 | { email => '*cpan.org' } ] 823 | }, 824 | ], 825 | } ); 826 | 827 | =head2 NOT 828 | 829 | If you want to filter out some of the results of an either/all query 830 | adding a I filter condition, such as "not these", you can use the 831 | following syntax with the C key: 832 | 833 | # any author named "Dave" or "David" 834 | my $daves = $mcpan->author( { 835 | either => [ 836 | { name => 'Dave *' }, 837 | { name => 'David *' }, 838 | ], 839 | not => [ 840 | { email => '*gmail.com' }, 841 | ], 842 | } ); 843 | 844 | =head1 DESIGN 845 | 846 | This module has three purposes: 847 | 848 | =over 4 849 | 850 | =item * Provide 100% of the MetaCPAN API 851 | 852 | This module will be updated regularly on every MetaCPAN API change, and intends 853 | to provide the user with as much of the API as possible, no shortcuts. If it's 854 | documented in the API, you should be able to do it. 855 | 856 | Because of this design decision, this module has an official MetaCPAN namespace 857 | with the blessing of the MetaCPAN developers. 858 | 859 | Notice this module currently only provides the beta API, not the old 860 | soon-to-be-deprecated API. 861 | 862 | =item * Be lightweight, to allow flexible usage 863 | 864 | While many modules would help make writing easier, it's important to take into 865 | account how they affect your compile-time, run-time, overall memory 866 | consumption, and CPU usage. 867 | 868 | By providing a slim interface implementation, more users are able to use this 869 | module, such as long-running processes (like daemons), CLI or GUI applications, 870 | cron jobs, and more. 871 | 872 | =item * DWIM 873 | 874 | While it's possible to access the methods defined by the API spec, there's still 875 | a matter of what you're really trying to achieve. For example, when searching 876 | for I<"Dave">, you want to find both I and I (and any 877 | other I), but you also want to search for a PAUSE ID of I, if one 878 | exists. 879 | 880 | This is where DWIM comes in. This module provides you with additional generic 881 | methods which will try to do what they think you want. 882 | 883 | Of course, this does not prevent you from manually using the API methods. You 884 | still have full control over that, if that's what you wish. 885 | 886 | You can (and should) read up on the general methods, which will explain how 887 | their DWIMish nature works, and what searches they run. 888 | 889 | =back 890 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Author.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Author; 4 | # ABSTRACT: An Author data object 5 | 6 | use Moo; 7 | use Ref::Util qw< is_arrayref >; 8 | 9 | with 'MetaCPAN::Client::Role::Entity'; 10 | 11 | my %known_fields = ( 12 | scalar => [qw< 13 | city 14 | country 15 | gravatar_url 16 | name 17 | ascii_name 18 | pauseid 19 | region 20 | updated 21 | user 22 | >], 23 | 24 | arrayref => [qw< 25 | donation 26 | email 27 | perlmongers 28 | profile 29 | website 30 | >], 31 | 32 | hashref => [qw< 33 | blog 34 | extra 35 | links 36 | release_count 37 | >], 38 | ); 39 | 40 | sub BUILDARGS { 41 | my ( $class, %args ) = @_; 42 | 43 | my $email = $args{'email'} || []; 44 | $args{'email'} = [ $email ] 45 | unless is_arrayref($email); 46 | 47 | return \%args; 48 | } 49 | 50 | my @known_fields = 51 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 52 | 53 | foreach my $field ( @known_fields ) { 54 | has $field => ( 55 | is => 'ro', 56 | lazy => 1, 57 | default => sub { 58 | my $self = shift; 59 | return $self->data->{$field}; 60 | }, 61 | ); 62 | } 63 | 64 | sub _known_fields { \%known_fields } 65 | 66 | sub releases { 67 | my $self = shift; 68 | my $id = $self->pauseid; 69 | 70 | return $self->client->release({ 71 | all => [ 72 | { author => $id }, 73 | { status => 'latest' }, 74 | ] 75 | }); 76 | } 77 | 78 | sub dir { $_[0]->links->{cpan_directory} } 79 | 80 | sub metacpan_url { "https://metacpan.org/author/" . $_[0]->pauseid } 81 | 82 | 1; 83 | 84 | __END__ 85 | 86 | =head1 SYNOPSIS 87 | 88 | my $author = $mcpan->author('MICKEY'); 89 | 90 | =head1 DESCRIPTION 91 | 92 | a MetaCPAN author entity object. 93 | 94 | =head1 ATTRIBUTES 95 | 96 | =head2 pauseid 97 | 98 | The author's pause id, which is a string like C or C. 99 | 100 | =head2 name 101 | 102 | The author's full name, if they've provided this in their MetaCPAN 103 | profile. This may contain Unicode characters. 104 | 105 | =head2 ascii_name 106 | 107 | An ASCII-only version of the author's full name, if they've provided this in 108 | their MetaCPAN profile. 109 | 110 | =head2 city 111 | 112 | The author's city, if they've provided this in their MetaCPAN profile. 113 | 114 | =head2 region 115 | 116 | The author's region, if they've provided this in their MetaCPAN profile. 117 | 118 | =head2 country 119 | 120 | The author's country, if they've provided this in their MetaCPAN profile. 121 | 122 | =head2 updated 123 | 124 | An ISO8601 datetime string like C<2016-11-19T12:41:46> indicating when the 125 | author last updated their MetaCPAN profile. This is always provided in UTC. 126 | 127 | =head2 dir 128 | 129 | The author's CPAN directory, which is something like C. 130 | 131 | =head2 gravatar_url 132 | 133 | The author's gravatar.com user URL, if they have one. This URL is generated 134 | using PAUSEID@cpan.org. 135 | 136 | =head2 user 137 | 138 | The user's internal MetaCPAN id. 139 | 140 | =head2 donation 141 | 142 | This is an arrayref containing zero or more hashrefs. Each hashref contains 143 | two keys, C and C. The known names are currently C, 144 | C, and C. The id will be an appropriate id or URL for the 145 | thing in question. 146 | 147 | This may be empty if the author has not provided this information in their 148 | MetaCPAN profile. 149 | 150 | For example: 151 | 152 | [ 153 | { "name" => "paypal", "id" => "brian.d.foy@gmail.com" }, 154 | { "name" => "wishlist", "id" => "http://amzn.com/w/4O7IX9ZNQJR" }, 155 | ], 156 | 157 | =head2 email 158 | 159 | This is an arrayref containing zero or more email addresses that the author 160 | has added to their MetaCPAN profile. Note that this does I include the 161 | C email address that all CPAN authors have. 162 | 163 | =head2 website 164 | 165 | This is an arrayref of website URLs provided by the author in their MetaCPAN 166 | profile. 167 | 168 | =head2 profile 169 | 170 | This is an arrayref containing zero or more hashrefs. Each hashref contains 171 | two keys, C and C. The names are things like C or 172 | C. The id will be an appropriate id for the site in question. 173 | 174 | For example: 175 | 176 | [ 177 | { name => "amazon", id => "B002MRC39U" }, 178 | { name => "stackoverflow", id => "brian-d-foy" }, 179 | ] 180 | 181 | This may be empty if the author has not provided this information in their 182 | MetaCPAN profile. 183 | 184 | =head2 perlmongers 185 | 186 | This is an arrayref containing zero or more hashrefs. Each hashref contains 187 | two keys, C and C. The names are things like C. 188 | 189 | This may be empty if the author has not provided this information in their 190 | MetaCPAN profile. 191 | 192 | =head2 links 193 | 194 | This is a hashref where the keys are a link type, and the values are URLs. The 195 | currently known keys are: 196 | 197 | =over 4 198 | 199 | =item * cpan_directory 200 | 201 | The author's CPAN directory. 202 | 203 | =item * cpantesters_reports 204 | 205 | The author's CPAN Testers Reports page. 206 | 207 | =item * cpantesters_matrix 208 | 209 | The author's CPAN Testers matrix page. 210 | 211 | =item * cpants 212 | 213 | The author's CPANTS page. 214 | 215 | =item * metacpan_explorer 216 | 217 | A link to the MetaCPAN explorer site pre-populated with a request for the 218 | author's profile. 219 | 220 | =back 221 | 222 | =head2 blog 223 | 224 | This is an arrayref containing zer or more hashrefs. Each hashref contains two 225 | keys, C and C. For example: 226 | 227 | { 228 | url => "http://blogs.perl.org/users/brian_d_foy/", 229 | feed => "http://blogs.perl.org/users/brian_d_foy/atom.xml", 230 | } 231 | 232 | =head2 release_count 233 | 234 | This is a hashref containing counts for various types of releases. The known 235 | keys are: 236 | 237 | =over 4 238 | 239 | =item * cpan 240 | 241 | The total number of distribution uplaods the author currently has on CPAN. 242 | 243 | =item * latest 244 | 245 | The total number of unique distributions the author currently has on CPAN. 246 | 247 | =item * backpan-only 248 | 249 | The number of distribution uploads currently only available via BackPAN. 250 | 251 | =back 252 | 253 | =head2 extra 254 | 255 | Returns a hashref. The contents of this are entirely arbitrary and will vary 256 | by author. 257 | 258 | =head1 METHODS 259 | 260 | =head2 BUILDARGS 261 | 262 | Ensures format of the input. 263 | 264 | =head2 releases 265 | 266 | my $releases = $author->releases(); 267 | 268 | This method returns a L of 269 | L objects. It includes all of the author's releases 270 | with the C status. 271 | 272 | =head2 metacpan_url 273 | 274 | Returns a link to the author's page on MetaCPAN. 275 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Cover.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Cover; 4 | # ABSTRACT: A Cover data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< distribution release version >], 12 | arrayref => [], 13 | hashref => [qw< criteria >], 14 | ); 15 | 16 | my @known_fields = 17 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 18 | 19 | foreach my $field (@known_fields) { 20 | has $field => ( 21 | is => 'ro', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | return $self->data->{$field}; 26 | }, 27 | ); 28 | } 29 | 30 | sub _known_fields { return \%known_fields } 31 | 32 | 1; 33 | 34 | __END__ 35 | 36 | =head1 SYNOPSIS 37 | 38 | my $cover = $mcpan->cover('Moose-2.2007'); 39 | 40 | =head1 DESCRIPTION 41 | 42 | A MetaCPAN cover entity object. 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 distribution 47 | 48 | Returns the name of the distribution. 49 | 50 | =head2 release 51 | 52 | Returns the name of the release. 53 | 54 | =head2 version 55 | 56 | Returns the version of the release. 57 | 58 | =head2 criteria 59 | 60 | Returns a hashref with the coverage stats for the release. 61 | Will contain one or more of the following keys: 62 | 'branch', 'condition', 'statement', 'subroutine', 'total' 63 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Distribution.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Distribution; 4 | # ABSTRACT: A Distribution data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< name >], 12 | arrayref => [], 13 | hashref => [qw< bugs river >] 14 | ); 15 | 16 | my %__known_fields_ex = ( 17 | map { my $k = $_; $k => +{ map { $_ => 1 } @{ $known_fields{$k} } } } 18 | keys %known_fields 19 | ); 20 | 21 | my @known_fields = map { @{ $known_fields{$_} } } keys %known_fields; 22 | 23 | foreach my $field ( @known_fields ) { 24 | has $field => ( 25 | is => 'ro', 26 | lazy => 1, 27 | default => sub { 28 | my $self = shift; 29 | return ( 30 | exists $self->data->{$field} ? $self->data->{$field} : 31 | exists $__known_fields_ex{hashref}{$field} ? {} : 32 | exists $__known_fields_ex{arrayref}{$field} ? [] : 33 | exists $__known_fields_ex{scalar}{$field} ? '' : 34 | undef 35 | ); 36 | }, 37 | ); 38 | } 39 | 40 | sub _known_fields { return \%known_fields } 41 | 42 | sub rt { $_[0]->bugs->{rt} || {} } 43 | sub github { $_[0]->bugs->{github} || {} } 44 | 45 | sub metacpan_url { "https://metacpan.org/release/" . $_[0]->name } 46 | 47 | 1; 48 | 49 | __END__ 50 | 51 | =head1 SYNOPSIS 52 | 53 | my $dist = $mcpan->distribution('MetaCPAN-Client'); 54 | 55 | =head1 DESCRIPTION 56 | 57 | A MetaCPAN distribution entity object. 58 | 59 | =head1 ATTRIBUTES 60 | 61 | =head2 name 62 | 63 | The distribution's name. 64 | 65 | =head2 bugs 66 | 67 | A hashref containing information about bugs reported in various issue 68 | trackers. The top-level keys are issue tracker names like C or 69 | C. Each value is itself a hashref containing information about the 70 | bugs in that tracker. The keys vary between trackers, but this will always 71 | contain a C key, which is a URL for the tracker. There may also be 72 | keys containing counts such as C, C, etc. 73 | 74 | =head2 river 75 | 76 | A hashref containing L<"CPAN 77 | River"|http://neilb.org/2015/04/20/river-of-cpan.html> information about the 78 | distro. The hashref contains the following keys: 79 | 80 | =over 4 81 | 82 | =item * bucket 83 | 84 | A positive or zero integer. The higher the number the farther upstream this 85 | distribution is. 86 | 87 | =item * immediate 88 | 89 | The number of distributions that directly depend on this one. 90 | 91 | =item * total 92 | 93 | The number of distributions that depend on this one, directly or indirectly. 94 | 95 | =back 96 | 97 | =head1 METHODS 98 | 99 | =head2 rt 100 | 101 | Returns the hashref of data for the rt bug tracker. This defaults to an empty 102 | hashref. 103 | 104 | =head2 github 105 | 106 | Returns the hashref of data for the github bug tracker. This defaults to an 107 | empty hashref. 108 | 109 | =head2 metacpan_url 110 | 111 | Returns a link to the distribution page on MetaCPAN. 112 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/DownloadURL.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::DownloadURL; 4 | # ABSTRACT: A Download URL data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< checksum_md5 checksum_sha256 date download_url status version >], 12 | arrayref => [], 13 | hashref => [], 14 | ); 15 | 16 | my @known_fields = 17 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 18 | 19 | foreach my $field (@known_fields) { 20 | has $field => ( 21 | is => 'ro', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | return $self->data->{$field}; 26 | }, 27 | ); 28 | } 29 | 30 | sub _known_fields { return \%known_fields } 31 | 32 | 1; 33 | 34 | __END__ 35 | 36 | =head1 SYNOPSIS 37 | 38 | my $download_url = $mcpan->download_url('Moose'); 39 | 40 | =head1 DESCRIPTION 41 | 42 | A MetaCPAN download_url entity object. 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 checksum_sha256 47 | 48 | The sha256 hexdigest for the file. 49 | 50 | =head2 checksum_md5 51 | 52 | The md5 hexdigest for the file. 53 | 54 | =head2 date 55 | 56 | Returns the date of the release that this URL refers to. 57 | 58 | =head2 download_url 59 | 60 | The actual download URL. 61 | 62 | =head2 status 63 | 64 | The release status, which is something like C or C 65 | 66 | =head2 version 67 | 68 | The version number for the distribution. 69 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Favorite.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Favorite; 4 | # ABSTRACT: A Favorite data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< date user release id author distribution >], 12 | arrayref => [], 13 | hashref => [] 14 | ); 15 | 16 | my @known_fields = 17 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 18 | 19 | foreach my $field (@known_fields) { 20 | has $field => ( 21 | is => 'ro', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | return $self->data->{$field}; 26 | }, 27 | ); 28 | } 29 | 30 | sub _known_fields { return \%known_fields } 31 | 32 | 1; 33 | 34 | __END__ 35 | 36 | =head1 SYNOPSIS 37 | 38 | # Query favorites for a given distribution: 39 | 40 | my $favorites = $mcpan->favorite( { 41 | distribution => 'Moose' 42 | } ); 43 | 44 | 45 | # Total number of matches ("how many favorites does the dist have?"): 46 | 47 | print $favorites->total; 48 | 49 | 50 | # Iterate over the favorite matches 51 | 52 | while ( my $fav = $favorites->next ) { ... } 53 | 54 | 55 | =head1 DESCRIPTION 56 | 57 | A MetaCPAN favorite entity object. 58 | 59 | =head1 ATTRIBUTES 60 | 61 | =head2 date 62 | 63 | An ISO8601 datetime string like C<2016-11-19T12:41:46> indicating when the 64 | favorite was created. 65 | 66 | =head2 user 67 | 68 | The user ID (B PAUSE ID) of the person who favorited the thing in 69 | question. 70 | 71 | =head2 release 72 | 73 | The release that was favorited. 74 | 75 | =head2 id 76 | 77 | The favorite ID. 78 | 79 | =head2 author 80 | 81 | The PAUSE ID of the author whose release was favorited. 82 | 83 | =head2 distribution 84 | 85 | The distribution that was favorited. 86 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/File.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::File; 4 | # ABSTRACT: A File data object 5 | 6 | use Moo; 7 | use Carp; 8 | 9 | with 'MetaCPAN::Client::Role::Entity'; 10 | 11 | my %known_fields = ( 12 | scalar => [qw< 13 | abstract 14 | author 15 | authorized 16 | binary 17 | date 18 | deprecated 19 | description 20 | directory 21 | distribution 22 | documentation 23 | download_url 24 | id 25 | indexed 26 | level 27 | maturity 28 | mime 29 | name 30 | path 31 | release 32 | sloc 33 | slop 34 | status 35 | version 36 | version_numified 37 | >], 38 | 39 | arrayref => [qw< module pod_lines >], 40 | 41 | hashref => [qw< stat >], 42 | ); 43 | 44 | my @known_fields = 45 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 46 | 47 | foreach my $field (@known_fields) { 48 | has $field => ( 49 | is => 'ro', 50 | lazy => 1, 51 | default => sub { 52 | my $self = shift; 53 | return $self->data->{$field}; 54 | }, 55 | ); 56 | } 57 | 58 | sub _known_fields { return \%known_fields } 59 | 60 | sub pod { 61 | my $self = shift; 62 | my $ctype = shift || "plain"; 63 | $ctype = lc($ctype); 64 | 65 | grep { $ctype eq $_ } qw 66 | or croak "wrong content-type for POD requested"; 67 | 68 | my $name = $self->module->[0]{name}; 69 | return unless $name; 70 | 71 | require MetaCPAN::Client::Request; 72 | 73 | return 74 | MetaCPAN::Client::Request->new->fetch( 75 | "pod/${name}?content-type=text/${ctype}" 76 | ); 77 | } 78 | 79 | sub source { 80 | my $self = shift; 81 | 82 | my $author = $self->author; 83 | my $release = $self->release; 84 | my $path = $self->path; 85 | 86 | require MetaCPAN::Client::Request; 87 | 88 | return 89 | MetaCPAN::Client::Request->new->fetch( 90 | "source/${author}/${release}/${path}" 91 | ); 92 | } 93 | 94 | sub metacpan_url { 95 | my $self = shift; 96 | sprintf("https://metacpan.org/source/%s/%s/%s", 97 | $self->author, $self->release, $self->path ); 98 | } 99 | 100 | 1; 101 | 102 | __END__ 103 | 104 | =head1 DESCRIPTION 105 | 106 | A MetaCPAN file entity object. 107 | 108 | =head1 ATTRIBUTES 109 | 110 | =head2 status 111 | 112 | Returns a release status like C, C, or C. 113 | 114 | =head2 date 115 | 116 | An ISO8601 datetime string like C<2016-11-19T12:41:46> indicating when the 117 | file was uploaded. 118 | 119 | =head2 author 120 | 121 | The author's PAUSE id. 122 | 123 | =head2 maturity 124 | 125 | This will be either C or C. 126 | 127 | =head2 directory 128 | 129 | A boolean indicating whether or not the path represents a directory. 130 | 131 | =head2 indexed 132 | 133 | A boolean indicating whether or not the content is indexed. 134 | 135 | =head2 documentation 136 | 137 | The name of the module for which this file contains docs. This may be C 138 | 139 | =head2 id 140 | 141 | The file's internal MetaCPAN id. 142 | 143 | =head2 authorized 144 | 145 | A boolean indicating whether or not this file was part of an authorized 146 | upload. 147 | 148 | =head2 version 149 | 150 | The distribution version that contains this file. 151 | 152 | =head2 version_numified 153 | 154 | The numified version of the distribution that contains the file. 155 | 156 | =head2 release 157 | 158 | The release that contains this file, which will be something like 159 | C. 160 | 161 | =head2 binary 162 | 163 | A boolean indicating whether or not this file contains binary content. 164 | 165 | =head2 name 166 | 167 | The File's name, without any directory path included. 168 | 169 | =head2 path 170 | 171 | The file's path I, relative to the root of 172 | the archive. 173 | 174 | =head2 abstract 175 | 176 | If the file contains POD with a C section, then this attribute will 177 | include the abstract portion of the name. 178 | 179 | =head2 deprecated 180 | 181 | The deprecated field value for this file. 182 | 183 | =head2 description 184 | 185 | If the file contains POD with a C section, then this attribute 186 | will contain that description. 187 | 188 | =head2 distribution 189 | 190 | The name of the distribution that contains the file. 191 | 192 | =head2 level 193 | 194 | A 0-indexed indication of how many directories deep this file is, relative to 195 | the archive root. 196 | 197 | =head2 sloc 198 | 199 | If the file contains code, this will contain the number of lines of code in 200 | the file. 201 | 202 | =head2 slop 203 | 204 | If the file contains POD, this will contain the number of lines of POD in 205 | the file. 206 | 207 | =head2 mime 208 | 209 | The file's mime type. 210 | 211 | =head2 module 212 | 213 | If the file contains module indexed by PAUSE, then this attribute contains an 214 | arrayref of hashrefs, one for each module. The hashrefs have the following 215 | keys: 216 | 217 | =over 4 218 | 219 | =item * name 220 | 221 | The module name. 222 | 223 | =item * indexed 224 | 225 | Whether or not the file is indexed by MetaCPAN. 226 | 227 | =item * authorized 228 | 229 | Whether or not the module is part of an authorized upload. 230 | 231 | =item * version 232 | 233 | The version of the module that this file contains. 234 | 235 | =item * version_numified 236 | 237 | The numified version of the module that this file contains. 238 | 239 | =item * associated_pod 240 | 241 | A path you can use with the C<< MetaCPAN::Client->file >> method to get the 242 | file that contains POD for this module. In most cases, that will be the same 243 | file as that one that contains this C data. 244 | 245 | =back 246 | 247 | =head2 pod_lines 248 | 249 | An arrayref. 250 | 251 | =head2 stat 252 | 253 | A hashref containing C all information about the file. The keys are: 254 | 255 | =over 4 256 | 257 | =item * mtime 258 | 259 | The Unix epoch of the file's last modified time. 260 | 261 | =item * mode 262 | 263 | The file's mode (as an integer, not an octal representation). 264 | 265 | =item * size 266 | 267 | The file's size in bytes. 268 | 269 | =back 270 | 271 | =head2 download_url 272 | 273 | A URL for the distribution archive that contains this file. 274 | 275 | =head1 METHODS 276 | 277 | =head2 pod 278 | 279 | my $pod = $module->pod(); # default = plain 280 | my $pod = $module->pod($type); 281 | 282 | Returns the POD content for the module/file. 283 | 284 | Takes a type as argument. 285 | 286 | Supported types: B, B, B, B. 287 | 288 | =head2 source 289 | 290 | my $source = $module->source(); 291 | 292 | Returns the source code for the file. 293 | 294 | =head2 metacpan_url 295 | 296 | Returns a link to the file source page on MetaCPAN. 297 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Mirror.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Mirror; 4 | # ABSTRACT: A Mirror data object 5 | 6 | use Moo; 7 | use Carp; 8 | 9 | with 'MetaCPAN::Client::Role::Entity'; 10 | 11 | my %known_fields = ( 12 | scalar => [qw< 13 | aka_name 14 | A_or_CNAME 15 | ccode 16 | city 17 | continent 18 | country 19 | dnsrr 20 | freq 21 | ftp 22 | http 23 | inceptdate 24 | name 25 | note 26 | org 27 | region 28 | reitredate 29 | rsync 30 | src 31 | tz 32 | >], 33 | 34 | arrayref => [qw< contact location >], 35 | 36 | hashref => [qw<>], 37 | ); 38 | 39 | my @known_fields = 40 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 41 | 42 | foreach my $field (@known_fields) { 43 | has $field => ( 44 | is => 'ro', 45 | lazy => 1, 46 | default => sub { 47 | my $self = shift; 48 | return $self->data->{$field}; 49 | }, 50 | ); 51 | } 52 | 53 | sub _known_fields { return \%known_fields } 54 | 55 | 1; 56 | 57 | __END__ 58 | 59 | =head1 SYNOPSIS 60 | 61 | my $mirror = $mcpan->mirror('eutelia.it'); 62 | 63 | =head1 DESCRIPTION 64 | 65 | A MetaCPAN mirror entity object. 66 | 67 | =head1 ATTRIBUTES 68 | 69 | =head2 name 70 | 71 | The name of the mirror, which is what you passed 72 | 73 | =head2 org 74 | 75 | The organization that maintains the mirror. 76 | 77 | =head2 ftp 78 | 79 | An FTP url for the mirror. 80 | 81 | =head2 rsync 82 | 83 | An rsync url for the mirror. 84 | 85 | =head2 src 86 | 87 | =head2 city 88 | 89 | The city where the mirror is located. 90 | 91 | =head2 country 92 | 93 | The name of the country where the mirror is located. 94 | 95 | =head2 ccode 96 | 97 | The ISO country code for the mirror's country. 98 | 99 | =head2 aka_name 100 | 101 | =head2 tz 102 | 103 | =head2 note 104 | 105 | =head2 dnsrr 106 | 107 | =head2 region 108 | 109 | =head2 inceptdate 110 | 111 | =head2 freq 112 | 113 | =head2 continent 114 | 115 | =head2 http 116 | 117 | =head2 reitredate 118 | 119 | =head2 A_or_CNAME 120 | 121 | =head2 contact 122 | 123 | Array-Ref. 124 | 125 | =head2 location 126 | 127 | Array-Ref. 128 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Module.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Module; 4 | # ABSTRACT: A Module data object 5 | 6 | use Moo; 7 | extends 'MetaCPAN::Client::File'; 8 | 9 | sub metacpan_url { 10 | my $self = shift; 11 | sprintf("https://metacpan.org/pod/release/%s/%s/%s", 12 | $self->author, $self->release, $self->path ); 13 | } 14 | 15 | sub package { 16 | my $self = shift; 17 | return $self->client->package( $self->documentation ); 18 | } 19 | 20 | sub permission { 21 | my $self = shift; 22 | return $self->client->permission( $self->documentation ); 23 | } 24 | 25 | 1; 26 | 27 | __END__ 28 | 29 | =head1 SYNOPSIS 30 | 31 | my $module = MetaCPAN::Client->new->module('Moo'); 32 | 33 | =head1 DESCRIPTION 34 | 35 | A MetaCPAN module entity object. 36 | 37 | This is currently the exact same as L. 38 | 39 | =head1 ATTRIBUTES 40 | 41 | Whatever L has. 42 | 43 | =head1 METHODS 44 | 45 | =head2 metacpan_url 46 | 47 | Returns a link to the module page on MetaCPAN. 48 | 49 | =head2 package 50 | 51 | Returns an L object for the module. 52 | 53 | =head2 permission 54 | 55 | Returns an L object for the module. 56 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Package.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Package; 4 | # ABSTRACT: A package data object (02packages.details entry) 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< author distribution dist_version file module_name version >], 12 | arrayref => [qw<>], 13 | hashref => [], 14 | ); 15 | 16 | my @known_fields = 17 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 18 | 19 | foreach my $field (@known_fields) { 20 | has $field => ( 21 | is => 'ro', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | return $self->data->{$field}; 26 | }, 27 | ); 28 | } 29 | 30 | sub _known_fields { return \%known_fields } 31 | 32 | 1; 33 | 34 | __END__ 35 | 36 | =head1 SYNOPSIS 37 | 38 | my $package = $mcpan->package('MooseX::Types'); 39 | 40 | =head1 DESCRIPTION 41 | 42 | A MetaCPAN package (02packages.details) entity object. 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 module_name 47 | 48 | Returns the name of the module. 49 | 50 | =head2 file 51 | 52 | The file path in CPAN for the module (latest release) 53 | 54 | =head2 distribution 55 | 56 | The distribution in which the module exist 57 | 58 | =head2 version 59 | 60 | The (latest) version of the module 61 | 62 | =head2 dist_version 63 | 64 | The (latest) version of the distribution 65 | 66 | =head2 author 67 | 68 | The pauseid of the release author 69 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Permission.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Permission; 4 | # ABSTRACT: A Permission data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< module_name owner >], 12 | arrayref => [qw< co_maintainers >], 13 | hashref => [], 14 | ); 15 | 16 | my @known_fields = 17 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 18 | 19 | foreach my $field (@known_fields) { 20 | has $field => ( 21 | is => 'ro', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | return $self->data->{$field}; 26 | }, 27 | ); 28 | } 29 | 30 | sub _known_fields { return \%known_fields } 31 | 32 | 1; 33 | 34 | __END__ 35 | 36 | =head1 SYNOPSIS 37 | 38 | my $permission = $mcpan->permission('MooseX::Types'); 39 | 40 | =head1 DESCRIPTION 41 | 42 | A MetaCPAN permission entity object. 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 module_name 47 | 48 | Returns the name of the module. 49 | 50 | =head2 owner 51 | 52 | The module owner (first-come permissions). 53 | 54 | =head2 co_maintainers 55 | 56 | Other maintainers with permissions to this module. 57 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Pod.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Pod; 4 | # ABSTRACT: A Pod object 5 | 6 | use Moo; 7 | use Carp; 8 | 9 | use MetaCPAN::Client::Types qw< Str >; 10 | 11 | has request => ( 12 | is => 'ro', 13 | handles => [qw], 14 | required => 1, 15 | ); 16 | 17 | has name => ( is => 'ro', required => 1 ); 18 | 19 | has url_prefix => ( 20 | is => 'ro', 21 | isa => Str, 22 | ); 23 | 24 | my @known_formats = qw< 25 | html plain x_pod x_markdown 26 | >; 27 | 28 | foreach my $format (@known_formats) { 29 | has $format => ( 30 | is => 'ro', 31 | lazy => 1, 32 | default => sub { 33 | my $self = shift; 34 | return $self->_request( $format ); 35 | }, 36 | ); 37 | } 38 | 39 | sub _request { 40 | my $self = shift; 41 | my $ctype = shift || "plain"; 42 | $ctype =~ s/_/-/; 43 | 44 | my $url = 'pod/' . $self->name . '?content-type=text/' . $ctype; 45 | $self->url_prefix and $url .= '&url_prefix=' . $self->url_prefix; 46 | 47 | return $self->request->fetch($url); 48 | } 49 | 50 | 51 | 1; 52 | 53 | __END__ 54 | 55 | =head1 SYNOPSIS 56 | 57 | use strict; 58 | use warnings; 59 | use MetaCPAN::Client; 60 | 61 | my $pod = MetaCPAN::Client->new->pod('Moo'); 62 | 63 | print $pod->html; 64 | 65 | =head1 DESCRIPTION 66 | 67 | A MetaCPAN pod entity object. 68 | 69 | =head1 ATTRIBUTES 70 | 71 | =head2 request 72 | 73 | A L object (created in L) 74 | 75 | =head2 name 76 | 77 | The name of the module (probably always the value passed to the pod() method) 78 | 79 | =head2 url_prefix 80 | 81 | Prefix to be passed through the url_prefix query parameter to the 'pod' endpoint 82 | 83 | =head2 x_pod 84 | 85 | The raw pod extracted from the file. 86 | 87 | =head2 html 88 | 89 | Formatted as an HTML chunk (No ...) 90 | 91 | =head2 x_markdown 92 | 93 | Converted to Markdown. 94 | 95 | =head2 plain 96 | 97 | Formatted as plain text. 98 | 99 | Get the plaintext version of the documentation 100 | 101 | $pod = MetaCPAN::Client->new->pod( "MetaCPAN::Client" ); 102 | print $pod->plain; 103 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Rating.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Rating; 4 | # ABSTRACT: A Rating data object 5 | 6 | use Moo; 7 | 8 | with 'MetaCPAN::Client::Role::Entity'; 9 | 10 | my %known_fields = ( 11 | scalar => [qw< 12 | author 13 | date 14 | details 15 | distribution 16 | helpful 17 | rating 18 | release 19 | user 20 | >], 21 | arrayref => [], 22 | hashref => [], 23 | ); 24 | 25 | my @known_fields = 26 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 27 | 28 | foreach my $field (@known_fields) { 29 | has $field => ( 30 | is => 'ro', 31 | lazy => 1, 32 | default => sub { 33 | my $self = shift; 34 | return $self->data->{$field}; 35 | }, 36 | ); 37 | } 38 | 39 | sub _known_fields { return \%known_fields } 40 | 41 | 1; 42 | 43 | __END__ 44 | 45 | =head1 SYNOPSIS 46 | 47 | my $ratings = $mcpan->rating({ distribution => "Moo" }); 48 | while ( my $rat = $ratings->next ) { ... } 49 | 50 | =head1 DESCRIPTION 51 | 52 | A MetaCPAN rating entity object. 53 | 54 | =head1 ATTRIBUTES 55 | 56 | =head2 date 57 | 58 | An ISO8601 datetime string like C<2016-11-19T12:41:46> indicating when the 59 | rating was created. 60 | 61 | =head2 release 62 | 63 | =head2 author 64 | 65 | =head2 details 66 | 67 | =head2 rating 68 | 69 | =head2 distribution 70 | 71 | =head2 helpful 72 | 73 | =head2 user 74 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Release.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Release; 4 | # ABSTRACT: A Release data object 5 | 6 | use Moo; 7 | use Ref::Util qw< is_hashref >; 8 | use JSON::MaybeXS qw< decode_json >; 9 | 10 | with 'MetaCPAN::Client::Role::Entity', 11 | 'MetaCPAN::Client::Role::HasUA'; 12 | 13 | my %known_fields = ( 14 | scalar => [qw< 15 | abstract 16 | archive 17 | author 18 | authorized 19 | checksum_md5 20 | checksum_sha256 21 | date 22 | deprecated 23 | distribution 24 | download_url 25 | first 26 | id 27 | maturity 28 | main_module 29 | name 30 | status 31 | version 32 | version_numified 33 | >], 34 | 35 | arrayref => [qw< 36 | dependency 37 | license 38 | provides 39 | >], 40 | 41 | hashref => [qw< 42 | metadata 43 | resources 44 | stat 45 | tests 46 | >], 47 | ); 48 | 49 | my @known_fields = 50 | map { @{ $known_fields{$_} } } qw< scalar arrayref hashref >; 51 | 52 | foreach my $field (@known_fields) { 53 | has $field => ( 54 | is => 'ro', 55 | lazy => 1, 56 | default => sub { 57 | my $self = shift; 58 | return $self->data->{$field}; 59 | }, 60 | ); 61 | } 62 | 63 | sub _known_fields { return \%known_fields } 64 | 65 | sub changes { 66 | my $self = shift; 67 | my $url = sprintf "https://fastapi.metacpan.org/changes/%s/%s", $self->author, $self->name; 68 | my $res = $self->ua->get($url); 69 | return unless is_hashref($res); 70 | my $content = decode_json $res->{'content'}; 71 | return $content->{'content'}; 72 | } 73 | 74 | sub metacpan_url { 75 | my $self = shift; 76 | sprintf( "https://metacpan.org/release/%s/%s", $self->author, $self->name ) 77 | } 78 | 79 | sub contributors { 80 | my $self = shift; 81 | my $url = sprintf( "https://fastapi.metacpan.org/release/contributors/%s/%s", $self->author, $self->name ); 82 | my $res = $self->ua->get($url); 83 | return unless is_hashref($res); 84 | my $content = decode_json $res->{'content'}; 85 | return $content->{'contributors'}; 86 | } 87 | 88 | 1; 89 | 90 | __END__ 91 | 92 | =head1 SYNOPSIS 93 | 94 | my $release = $mcpan->release('Moose'); 95 | 96 | =head1 DESCRIPTION 97 | 98 | A MetaCPAN release entity object. 99 | 100 | =head1 ATTRIBUTES 101 | 102 | =head2 status 103 | 104 | The release's status, C, C, or C. 105 | 106 | =head2 name 107 | 108 | The release's name, something like C. 109 | 110 | =head2 date 111 | 112 | An ISO8601 datetime string like C<2016-11-19T12:41:46> indicating when the 113 | release was uploaded. 114 | 115 | =head2 author 116 | 117 | The PAUSE ID of the author who uploaded the release. 118 | 119 | =head2 maturity 120 | 121 | This will be either C or C. 122 | 123 | =head2 main_module 124 | 125 | The release's main module name. 126 | 127 | =head2 id 128 | 129 | The release's internal MetaCPAN id. 130 | 131 | =head2 authorized 132 | 133 | A boolean indicating whether or not this was an authorized release. 134 | 135 | =head2 download_url 136 | 137 | A URL for this release's distribution archive file. 138 | 139 | =head2 checksum_sha256 140 | 141 | The sha256 hexdigest for this release's distribution archive file. 142 | 143 | =head2 checksum_md5 144 | 145 | The md5 hexdigest for this release's distribution archive file. 146 | 147 | =head2 first 148 | 149 | A boolean indicating whether or not this is the first release of this 150 | distribution. 151 | 152 | =head2 archive 153 | 154 | The filename of the archive file for this release. 155 | 156 | =head2 version 157 | 158 | The release's version. 159 | 160 | =head2 version_numified 161 | 162 | The numified form of the release's version. 163 | 164 | =head2 deprecated 165 | 166 | The deprecated field value for this release. 167 | 168 | =head2 distribution 169 | 170 | The name of the distribution to which this release belongs. Something like C 171 | 172 | =head2 abstract 173 | 174 | The abstract from this release's metadata. 175 | 176 | =head2 dependency 177 | 178 | This is an arrayref of hashrefs. Each hashref contains the following keys: 179 | 180 | =over 4 181 | 182 | =item * phase 183 | 184 | The phase to which this dependency belongs. This will be one of C, 185 | C, C, C, or C. 186 | 187 | =item * relationship 188 | 189 | This will be one of C, C, or C. 190 | 191 | =item * module 192 | 193 | The name of the module which is depended on. 194 | 195 | =item * version 196 | 197 | The required version of the dependency. This may be C<0>, indicating that any 198 | version is acceptable. 199 | 200 | =back 201 | 202 | =head2 license 203 | 204 | An arrayref containing the license(s) under which this release has been made 205 | available. These licenses are represented by strings like C or 206 | C. 207 | 208 | =head2 provides 209 | 210 | This an arrayref containing a list of all the modules provided by this distribution. 211 | 212 | =head2 metadata 213 | 214 | This is a hashref containing metadata provided by the distribution. The exact 215 | contents of this hashref will vary across CPAN, but should largely conform to 216 | the spec defined by L. 217 | 218 | =head2 resources 219 | 220 | The resources portion of the release's metadata, returned as a hashref. 221 | 222 | =head2 stat 223 | 224 | A hashref containing C all information about the release's archive 225 | file. The keys are: 226 | 227 | =over 4 228 | 229 | =item * mtime 230 | 231 | The Unix epoch of the file's last modified time. 232 | 233 | =item * mode 234 | 235 | The file's mode (as an integer, not an octal representation). 236 | 237 | =item * size 238 | 239 | The file's size in bytes. 240 | 241 | =back 242 | 243 | =head2 tests 244 | 245 | Returns a hashref of information about CPAN testers results for this 246 | release. The keys are C, C, C, and C. The values are 247 | the count of that particular result on CPAN Testers for this release. 248 | 249 | =head1 METHODS 250 | 251 | =head2 changes 252 | 253 | Returns the Changes text for the release. 254 | 255 | =head2 metacpan_url 256 | 257 | Returns a link to the release page on MetaCPAN. 258 | 259 | =head2 contributors 260 | 261 | Returns a structure with release contributors info. 262 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Request.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Request; 4 | # ABSTRACT: Object used for making requests to MetaCPAN 5 | 6 | use Moo; 7 | use Carp; 8 | use JSON::MaybeXS qw; 9 | use Ref::Util qw< is_arrayref is_hashref is_ref >; 10 | 11 | use MetaCPAN::Client::Scroll; 12 | use MetaCPAN::Client::Types qw< HashRef Int >; 13 | 14 | with 'MetaCPAN::Client::Role::HasUA'; 15 | 16 | has _clientinfo => ( 17 | is => 'ro', 18 | isa => HashRef, 19 | lazy => 1, 20 | builder => '_build_clientinfo', 21 | ); 22 | 23 | has domain => ( 24 | is => 'ro', 25 | default => sub { 26 | $ENV{METACPAN_DOMAIN} and return $ENV{METACPAN_DOMAIN}; 27 | $_[0]->_clientinfo->{production}{domain}; 28 | }, 29 | ); 30 | 31 | has base_url => ( 32 | is => 'ro', 33 | lazy => 1, 34 | default => sub { 35 | $ENV{METACPAN_DOMAIN} and return $ENV{METACPAN_DOMAIN}; 36 | $_[0]->_clientinfo->{production}{url}; 37 | }, 38 | ); 39 | 40 | has _is_agg => ( 41 | is => 'ro', 42 | default => 0, 43 | writer => '_set_is_agg' 44 | ); 45 | 46 | has debug => ( 47 | is => 'ro', 48 | isa => Int, 49 | default => 0, 50 | ); 51 | 52 | sub BUILDARGS { 53 | my ( $self, %args ) = @_; 54 | $args{domain} and $args{base_url} = $args{domain}; 55 | return \%args; 56 | } 57 | 58 | sub _build_clientinfo { 59 | my $self = shift; 60 | 61 | my $info; 62 | eval { 63 | $info = $self->ua->get( 'https://clientinfo.metacpan.org' ); 64 | $info = decode_json( $info->{content} ); 65 | is_hashref($info) and exists $info->{production} or die; 66 | 1; 67 | } 68 | or $info = +{ 69 | production => { 70 | url => 'https://fastapi.metacpan.org', # last known production url 71 | domain => 'https://fastapi.metacpan.org', # last known production domain 72 | } 73 | }; 74 | 75 | return $info; 76 | } 77 | 78 | sub fetch { 79 | my $self = shift; 80 | my $url = shift or croak 'fetch must be called with a URL parameter'; 81 | my $params = shift || {}; 82 | $url =~ s{^/}{}; 83 | my $req_url = sprintf '%s/%s', $self->base_url, $url; 84 | my $ua = $self->ua; 85 | 86 | my $result = keys %{$params} 87 | ? $ua->post( $req_url, { content => encode_json $params } ) 88 | : $ua->get($req_url); 89 | 90 | return $self->_decode_result( $result, $req_url ); 91 | } 92 | 93 | sub ssearch { 94 | my $self = shift; 95 | my $type = shift; 96 | my $args = shift; 97 | my $params = shift; 98 | 99 | my $time = delete $params->{'scroller_time'} || '5m'; 100 | my $size = delete $params->{'scroller_size'} || 1000; 101 | 102 | my $scroller = MetaCPAN::Client::Scroll->new( 103 | ua => $self->ua, 104 | size => $size, 105 | time => $time, 106 | base_url => $self->base_url, 107 | type => $type, 108 | body => $self->_build_body($args, $params), 109 | debug => $self->debug, 110 | ); 111 | 112 | return $scroller; 113 | } 114 | 115 | sub _decode_result { 116 | my $self = shift; 117 | my $result = shift; 118 | my $url = shift or croak 'Second argument of a URL must be provided'; 119 | 120 | is_hashref($result) 121 | or croak 'First argument must be hashref'; 122 | 123 | my $success = $result->{'success'}; 124 | 125 | defined $success 126 | or croak 'Missing success in return value'; 127 | 128 | if (!$success) { 129 | my $reason_field = $result->{status} == 599 ? 'content' : 'reason'; 130 | croak "Failed to fetch '$url': " . $result->{$reason_field}; 131 | } 132 | 133 | my $content = $result->{'content'} 134 | or croak 'Missing content in return value'; 135 | 136 | $url =~ m|/pod/| and return $content; 137 | $url =~ m|/source/| and return $content; 138 | 139 | my $decoded_result; 140 | eval { 141 | $decoded_result = decode_json $content; 142 | 1; 143 | } or do { 144 | croak "Couldn't decode '$content': $@"; 145 | }; 146 | 147 | return $decoded_result; 148 | } 149 | 150 | sub _build_body { 151 | my $self = shift; 152 | my $args = shift; 153 | my $params = shift; 154 | 155 | my $query = $args->{__MATCH_ALL__} 156 | ? { match_all => {} } 157 | : _build_query_rec($args); 158 | 159 | return +{ 160 | query => $query, 161 | $self->_read_filters($params), 162 | $self->_read_fields($params), 163 | $self->_read_aggregations($params), 164 | $self->_read_sort($params) 165 | }; 166 | } 167 | 168 | my %key2es = ( 169 | all => 'must', 170 | either => 'should', 171 | not => 'must_not', 172 | ); 173 | 174 | sub _read_fields { 175 | my $self = shift; 176 | my $params = shift; 177 | 178 | my $fields = delete $params->{fields}; 179 | my $_source = delete $params->{_source}; 180 | 181 | my @ret; 182 | 183 | if ( $fields ) { 184 | is_arrayref($fields) or 185 | croak "fields must be an arrayref"; 186 | push @ret => ( fields => $fields ); 187 | } 188 | 189 | if ( $_source ) { 190 | is_arrayref($_source) or !is_ref($_source) or 191 | croak "_source must be an arrayref or a string"; 192 | push @ret => ( _source => $_source ); 193 | } 194 | 195 | return @ret; 196 | } 197 | 198 | sub _read_aggregations { 199 | my $self = shift; 200 | my $params = shift; 201 | 202 | my $aggregations = delete $params->{aggregations}; 203 | is_ref($aggregations) or return (); 204 | 205 | $self->_set_is_agg(1); 206 | return ( aggregations => $aggregations ); 207 | } 208 | 209 | sub _read_filters { 210 | my $self = shift; 211 | my $params = shift; 212 | 213 | my $filter = delete $params->{es_filter}; 214 | is_ref($filter) or return (); 215 | 216 | return ( filter => $filter ); 217 | } 218 | 219 | sub _read_sort { 220 | my $self = shift; 221 | my $params = shift; 222 | 223 | my $sort = delete $params->{sort}; 224 | is_ref($sort) or return (); 225 | 226 | return ( sort => $sort ); 227 | } 228 | 229 | sub _build_query_rec { 230 | my $args = shift; 231 | is_hashref($args) or croak 'query args must be a hash'; 232 | 233 | my %query = (); 234 | my $basic_element = 1; 235 | 236 | KEY: for my $k ( qw/ all either not / ) { 237 | my $v = delete $args->{$k} || next KEY; 238 | is_hashref($v) and $v = [ $v ]; 239 | is_arrayref($v) or croak "invalid value for key $k"; 240 | 241 | undef $basic_element; 242 | 243 | $query{'bool'}{ $key2es{$k} } = 244 | [ map +( _build_query_rec($_) ), @$v ]; 245 | 246 | $k eq 'either' and $query{'bool'}{'minimum_should_match'} = 1; 247 | } 248 | 249 | $basic_element and %query = %{ _build_query_element($args) }; 250 | 251 | return \%query; 252 | } 253 | 254 | sub _build_query_element { 255 | my $args = shift; 256 | 257 | scalar keys %{$args} == 1 258 | or croak 'Wrong number of keys in query element'; 259 | 260 | my ($key) = keys %{$args}; 261 | my $val = $args->{$key}; 262 | 263 | !is_ref($val) and $val =~ /[\w\*]/ 264 | or croak 'Wrong type of query arguments'; 265 | 266 | my $wildcard = $val =~ /[*?]/; 267 | my $qtype = $wildcard ? 'wildcard' : 'term'; 268 | 269 | return +{ $qtype => $args }; 270 | } 271 | 272 | 273 | 1; 274 | 275 | __END__ 276 | 277 | =head1 ATTRIBUTES 278 | 279 | =head2 domain 280 | 281 | $mcpan = MetaCPAN::Client->new( domain => 'localhost' ); 282 | 283 | What domain to use for all requests. 284 | 285 | Default: B. 286 | 287 | =head2 base_url 288 | 289 | my $mcpan = MetaCPAN::Client->new( 290 | base_url => 'https://localhost:9999/v2', 291 | ); 292 | 293 | Instead of overriding the C, you should override the C. 294 | The C will be set appropriately automatically. 295 | 296 | Default: I. 297 | 298 | =head2 debug 299 | 300 | debug-mode for more detailed error messages. 301 | 302 | =head1 METHODS 303 | 304 | =head2 BUILDARGS 305 | 306 | =head2 fetch 307 | 308 | my $result = $mcpan->fetch('/release/Moose'); 309 | 310 | # with parameters 311 | my $more = $mcpan->fetch( 312 | '/release/Moose', 313 | { param => 'value' }, 314 | ); 315 | 316 | Fetches a path from MetaCPAN (post or get), and returns the decoded result. 317 | 318 | =head2 ssearch 319 | 320 | Calls an Elasticsearch query and returns an L 321 | scroller object. 322 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/ResultSet.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::ResultSet; 4 | # ABSTRACT: A Result Set 5 | 6 | use Moo; 7 | use Carp; 8 | 9 | use MetaCPAN::Client::Types qw< ArrayRef >; 10 | 11 | has type => ( 12 | is => 'ro', 13 | isa => sub { 14 | croak 'Invalid type' unless 15 | grep { $_ eq $_[0] } qw; 17 | }, 18 | lazy => 1, 19 | ); 20 | 21 | # in case we're returning from a scrolled search 22 | has scroller => ( 23 | is => 'ro', 24 | isa => sub { 25 | use Safe::Isa; 26 | $_[0]->$_isa('MetaCPAN::Client::Scroll') 27 | or croak 'scroller must be an MetaCPAN::Client::Scroll object'; 28 | }, 29 | predicate => 'has_scroller', 30 | ); 31 | 32 | # in case we're returning from a fetch 33 | has items => ( 34 | is => 'ro', 35 | isa => ArrayRef, 36 | ); 37 | 38 | has total => ( 39 | is => 'ro', 40 | default => sub { 41 | my $self = shift; 42 | 43 | return $self->has_scroller ? $self->scroller->total 44 | : scalar @{ $self->items }; 45 | }, 46 | ); 47 | 48 | has 'class' => ( 49 | is => 'ro', 50 | lazy => 1, 51 | builder => '_build_class', 52 | ); 53 | 54 | sub BUILDARGS { 55 | my ( $class, %args ) = @_; 56 | 57 | exists $args{scroller} or exists $args{items} 58 | or croak 'ResultSet must get either scroller or items'; 59 | 60 | exists $args{scroller} and exists $args{items} 61 | and croak 'ResultSet must get either scroller or items, not both'; 62 | 63 | exists $args{type} or exists $args{class} 64 | or croak 'Must pass either type or target class to ResultSet'; 65 | 66 | exists $args{type} and exists $args{class} 67 | and croak 'Must pass either type or target class to ResultSet, not both'; 68 | 69 | return \%args; 70 | } 71 | sub BUILD { 72 | my ( $self ) = @_; 73 | $self->class; # vifify and validate 74 | } 75 | 76 | sub next { 77 | my $self = shift; 78 | my $result = $self->has_scroller ? $self->scroller->next 79 | : shift @{ $self->items }; 80 | 81 | defined $result or return; 82 | 83 | return $self->class->new_from_request( $result->{'_source'} || $result->{'fields'} || $result ); 84 | } 85 | 86 | sub aggregations { 87 | my $self = shift; 88 | 89 | return $self->has_scroller ? $self->scroller->aggregations : {}; 90 | } 91 | 92 | sub _build_class { 93 | my $self = shift; 94 | return 'MetaCPAN::Client::' . ucfirst $self->type; 95 | } 96 | 97 | 1; 98 | 99 | __END__ 100 | 101 | =head1 DESCRIPTION 102 | 103 | Object representing a result from Elastic Search. This is used for the complex 104 | (as in L) queries to MetaCPAN. It 105 | provides easy access to the scroller and aggregations. 106 | 107 | =head1 ATTRIBUTES 108 | 109 | =head2 scroller 110 | 111 | An L object. 112 | 113 | =head2 items 114 | 115 | An arrayref of items to manually scroll over, instead of a scroller object. 116 | 117 | =head2 type 118 | 119 | The entity of the result set. Available types: 120 | 121 | =over 4 122 | 123 | =item * author 124 | 125 | =item * distribution 126 | 127 | =item * module 128 | 129 | =item * release 130 | 131 | =item * favorite 132 | 133 | =item * file 134 | 135 | =back 136 | 137 | =head2 aggregations 138 | 139 | The aggregations available in the Elastic Search response. 140 | 141 | =head1 METHODS 142 | 143 | =head2 next 144 | 145 | Iterator call to fetch the next result set object. 146 | 147 | =head2 total 148 | 149 | Iterator call to fetch the total amount of objects available in result set. 150 | 151 | =head2 has_scroller 152 | 153 | Predicate for ES scroller presence. 154 | 155 | =head2 BUILDARGS 156 | 157 | Double checks construction of objects. You should never run this yourself. 158 | 159 | =head2 BUILD 160 | 161 | Validates the object. You should never run this yourself. 162 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Role/Entity.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Role::Entity; 4 | # ABSTRACT: A role for MetaCPAN entities 5 | 6 | use Moo::Role; 7 | 8 | use JSON::PP; 9 | use Ref::Util qw< is_ref is_arrayref is_hashref >; 10 | 11 | has data => ( 12 | is => 'ro', 13 | required => 1, 14 | ); 15 | 16 | has client => ( 17 | is => 'ro', 18 | lazy => 1, 19 | builder => '_build_client', 20 | ); 21 | 22 | sub _build_client { 23 | require MetaCPAN::Client; 24 | return MetaCPAN::Client->new(); 25 | } 26 | 27 | sub BUILDARGS { 28 | my ( $class, %args ) = @_; 29 | 30 | my $known_fields = $class->_known_fields; 31 | 32 | for my $k ( @{ $known_fields->{scalar} } ) { 33 | $args{data}{$k} = $args{data}{$k}->[0] 34 | if is_arrayref( $args{data}{$k} ) and @{$args{data}{$k}} == 1; 35 | 36 | if ( JSON::PP::is_bool($args{data}{$k}) ) { 37 | $args{data}{$k} = !!$args{data}{$k}; 38 | } 39 | elsif ( is_ref( $args{data}{$k} ) ) { 40 | delete $args{data}{$k}; 41 | } 42 | } 43 | 44 | for my $k ( @{ $known_fields->{arrayref} } ) { 45 | # fix the case when we expect an array ref but get a scalar because 46 | # the result array had one element and we received a scalar 47 | if ( defined($args{data}{$k}) and !is_ref($args{data}{$k}) ) { 48 | $args{data}{$k} = [ $args{data}{$k} ] 49 | } 50 | 51 | delete $args{data}{$k} 52 | unless is_arrayref( $args{data}{$k} ); # warn? 53 | } 54 | 55 | for my $k ( @{ $known_fields->{hashref} } ) { 56 | delete $args{data}{$k} 57 | unless is_hashref( $args{data}{$k} ); # warn? 58 | } 59 | 60 | return \%args; 61 | } 62 | 63 | sub new_from_request { 64 | my ( $class, $request, $client ) = @_; 65 | 66 | my $known_fields = $class->_known_fields; 67 | 68 | return $class->new( 69 | ( defined $client ? ( client => $client ) : () ), 70 | data => { 71 | map +( defined $request->{$_} ? ( $_ => $request->{$_} ) : () ), 72 | map +( @{ $known_fields->{$_} } ), 73 | qw< scalar arrayref hashref > 74 | } 75 | ); 76 | } 77 | 78 | 1; 79 | 80 | __END__ 81 | 82 | =head1 DESCRIPTION 83 | 84 | This is a role to be consumed by all L entities. It provides 85 | common attributes and methods. 86 | 87 | =head1 ATTRIBUTES 88 | 89 | =head2 data 90 | 91 | Hash reference containing all the entity data. 92 | 93 | Entities are usually generated using C which sets the C 94 | attribute appropriately by picking the relevant information. 95 | 96 | Required. 97 | 98 | =head1 METHODS 99 | 100 | =head2 new_from_request 101 | 102 | Create a new entity object using a request hash. The hash represents the 103 | information returned from a MetaCPAN request. This also sets the data attribute. 104 | 105 | =head2 BUILDARGS 106 | 107 | Perform type checks & conversion for the incoming data. 108 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Role/HasUA.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Role::HasUA; 4 | # ABSTRACT: Role for supporting user-agent attribute 5 | 6 | use Moo::Role; 7 | use Carp; 8 | use HTTP::Tiny; 9 | 10 | has _user_ua => ( 11 | init_arg => 'ua', 12 | is => 'ro', 13 | predicate => '_has_user_ua', 14 | ); 15 | 16 | has ua => ( 17 | init_arg => undef, 18 | is => 'ro', 19 | lazy => 1, 20 | builder => '_build_ua', 21 | ); 22 | 23 | has ua_args => ( 24 | is => 'ro', 25 | default => sub { 26 | [ agent => 'MetaCPAN::Client/'.($MetaCPAN::Client::VERSION||'xx'), 27 | verify_SSL => 1 ] 28 | }, 29 | ); 30 | 31 | sub _build_ua { 32 | my $self = shift; 33 | 34 | # This level of indirection is so that if a user has not specified a custom UA 35 | # MetaCPAN::Client will have its own UA's 36 | # 37 | # But if the user **has** specified a custom UA, that UA is used for both. 38 | if ( $self->_has_user_ua ) { 39 | my $ua = $self->_user_ua; 40 | croak "cannot use given ua (must support 'get' and 'post' methods)" 41 | unless $ua->can("get") and $ua->can("post"); 42 | 43 | return $self->_user_ua; 44 | } 45 | 46 | return HTTP::Tiny->new( @{ $self->ua_args } ); 47 | } 48 | 49 | 1; 50 | __END__ 51 | 52 | =head1 ATTRIBUTES 53 | 54 | =head2 ua 55 | 56 | my $mcpan = MetaCPAN::Client->new( ua => HTTP::Tiny->new(...) ); 57 | 58 | The user agent object for running requests. 59 | 60 | It must provide an interface that matches L. Explicitly: 61 | 62 | =over 4 63 | 64 | =item * Implement post() 65 | 66 | Method C must be available that accepts a request URL and a hashref of 67 | options. 68 | 69 | =item * Implement get() 70 | 71 | Method C must be available that accepts a request URL. 72 | 73 | =item * Return result hashref 74 | 75 | Must return a result hashref which has key C and key C. 76 | 77 | =back 78 | 79 | Default: L, 80 | 81 | =head2 ua_args 82 | 83 | my $mcpan = MetaCPAN::Client->new( 84 | ua_args => [ agent => 'MyAgent' ], 85 | ); 86 | 87 | Arguments sent to the user agent. 88 | 89 | Default: user agent string: B. 90 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Scroll.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Scroll; 4 | # ABSTRACT: A MetaCPAN::Client scroller 5 | 6 | use Moo; 7 | use Carp; 8 | use Ref::Util qw< is_hashref >; 9 | use JSON::MaybeXS qw< decode_json encode_json >; 10 | 11 | use MetaCPAN::Client::Types qw< Str Int Time ArrayRef HashRef Bool >; 12 | 13 | has ua => ( 14 | is => 'ro', 15 | required => 1, 16 | ); 17 | 18 | has size => ( 19 | is => 'ro', 20 | isa => Str, 21 | ); 22 | 23 | has time => ( 24 | is => 'ro', 25 | isa => Time, 26 | ); 27 | 28 | has base_url => ( 29 | is => 'ro', 30 | isa => Str, 31 | required => 1, 32 | ); 33 | 34 | has type => ( 35 | is => 'ro', 36 | isa => Str, 37 | required => 1, 38 | ); 39 | 40 | has body => ( 41 | is => 'ro', 42 | isa => HashRef, 43 | required => 1, 44 | ); 45 | 46 | has _id => ( 47 | is => 'ro', 48 | isa => Str, 49 | ); 50 | 51 | has _buffer => ( 52 | is => 'ro', 53 | isa => ArrayRef, 54 | default => sub { [] }, 55 | ); 56 | 57 | has _remaining => ( 58 | is => 'rw', 59 | isa => Int, 60 | default => sub { 0 }, 61 | ); 62 | 63 | has total => ( 64 | is => 'ro', 65 | isa => Int, 66 | ); 67 | 68 | has aggregations => ( 69 | is => 'ro', 70 | isa => HashRef, 71 | default => sub { +{} }, 72 | ); 73 | 74 | sub BUILDARGS { 75 | my ( $class, %args ) = @_; 76 | $args{time} //= '5m'; 77 | $args{size} //= '100'; 78 | 79 | my ( $ua, $base_url, $type, $body, $time, $size ) = 80 | @args{qw< ua base_url type body time size >}; 81 | 82 | # fetch scroller from server 83 | 84 | my $res = $ua->post( 85 | sprintf( '%s/%s/_search?scroll=%s&size=%s', $base_url, $type, $time, $size ), 86 | { content => encode_json $body } 87 | ); 88 | 89 | if ( $res->{status} != 200 ) { 90 | my $msg = "failed to create a scrolled search"; 91 | $args{debug} and $msg .= "\n(" . $res->{content} . ")"; 92 | croak $msg; 93 | } 94 | 95 | my $content = decode_json $res->{content}; 96 | 97 | # read response content --> object params 98 | 99 | $args{_id} = $content->{_scroll_id}; 100 | $args{total} = $content->{hits}{total}; 101 | $args{_buffer} = $content->{hits}{hits}; 102 | $args{_remaining} = $content->{hits}{total}; 103 | 104 | $args{aggregations} = $content->{aggregations} 105 | if $content->{aggregations} and is_hashref( $content->{aggregations} ); 106 | 107 | return \%args; 108 | } 109 | 110 | sub next { 111 | my $self = shift; 112 | my $buffer = $self->_buffer; 113 | my $remaining = $self->_remaining; 114 | 115 | if (!$remaining) { 116 | # We're exhausted and will do no more. 117 | return undef; 118 | } 119 | elsif (!@$buffer) { 120 | # Refill the buffer if it's empty. 121 | @$buffer = @{ $self->_fetch_next }; 122 | 123 | if (!@$buffer) { 124 | # we weren't able to refill for some reason 125 | $self->_remaining(0); 126 | return undef; 127 | } 128 | } 129 | 130 | # One less result to return 131 | $self->_remaining($remaining - 1); 132 | # Return the next result 133 | return shift @$buffer; 134 | } 135 | 136 | sub _fetch_next { 137 | my $self = shift; 138 | 139 | my $res = $self->ua->post( 140 | sprintf( '%s/_search/scroll?scroll=%s&size=%s', $self->base_url, $self->time, $self->size ), 141 | { content => $self->_id } 142 | ); 143 | 144 | croak "failed to fetch next scrolled batch" 145 | unless $res->{status} == 200; 146 | 147 | my $content = decode_json $res->{content}; 148 | 149 | return $content->{hits}{hits}; 150 | } 151 | 152 | sub DEMOLISH { 153 | my ( $self, $gd ) = @_; 154 | return 155 | if $gd; 156 | my $ua = $self->ua 157 | or return; 158 | my $base_url = $self->base_url 159 | or return; 160 | my $id = $self->_id 161 | or return; 162 | my $time = $self->time 163 | or return; 164 | 165 | $ua->delete( 166 | sprintf( '%s/_search/scroll?scroll=%s', $base_url, $time ), 167 | { content => $id } 168 | ); 169 | } 170 | 171 | 1; 172 | __END__ 173 | 174 | =head1 METHODS 175 | 176 | =head2 next 177 | 178 | get next matched document. 179 | 180 | =head2 BUILDARGS 181 | 182 | =head2 DEMOLISH 183 | 184 | =head1 ATTRIBUTES 185 | 186 | =head2 aggregations 187 | 188 | The returned aggregations structure from agg 189 | requests. 190 | 191 | =head2 base_url 192 | 193 | The base URL for sending server requests. 194 | 195 | =head2 body 196 | 197 | The request body. 198 | 199 | =head2 size 200 | 201 | The number of docs to pull from each shard per request. 202 | 203 | =head2 time 204 | 205 | The lifetime of the scroller on the server. 206 | 207 | =head2 total 208 | 209 | The total number of matches. 210 | 211 | =head2 type 212 | 213 | The Elasticsearch type to query. 214 | 215 | =head2 ua 216 | 217 | The user agent object for running requests. 218 | -------------------------------------------------------------------------------- /lib/MetaCPAN/Client/Types.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | package MetaCPAN::Client::Types; 4 | # ABSTRACT: type checking helper class 5 | 6 | use Type::Tiny (); 7 | use Types::Standard (); 8 | use Ref::Util qw< is_ref >; 9 | 10 | use parent 'Exporter'; 11 | our @EXPORT_OK = qw< Str Int Time ArrayRef HashRef Bool >; 12 | 13 | sub Str { Types::Standard::Str } 14 | sub Int { Types::Standard::Int } 15 | sub ArrayRef { Types::Standard::ArrayRef } 16 | sub HashRef { Types::Standard::HashRef } 17 | sub Bool { Types::Standard::Bool } 18 | 19 | sub Time { 20 | return Type::Tiny->new( 21 | name => "Time", 22 | constraint => sub { !is_ref($_) and /^[1-9][0-9]*(?:s|m|h)$/ }, 23 | message => sub { "$_ is not in time format (e.g. '5m')" }, 24 | ); 25 | } 26 | 27 | 1; 28 | __END__ 29 | 30 | =head1 METHODS 31 | 32 | =head2 ArrayRef 33 | 34 | =head2 Bool 35 | 36 | =head2 HashRef 37 | 38 | =head2 Int 39 | 40 | =head2 Str 41 | 42 | =head2 Time 43 | -------------------------------------------------------------------------------- /t/api/_get.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 13; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | { 12 | no warnings qw; 13 | 14 | *MetaCPAN::Client::Author::new_from_request = sub { 15 | my ( $self, $res ) = @_; 16 | ::isa_ok( $self, 'MetaCPAN::Client::Author' ); 17 | ::is_deeply( $res, { hello => 'world' }, 'Correct response' ); 18 | 19 | return 'ok'; 20 | }; 21 | 22 | my $count = 0; 23 | *MetaCPAN::Client::fetch = sub { 24 | my ( $self, $path ) = @_; 25 | ::isa_ok( $self, 'MetaCPAN::Client' ); 26 | ::is( $path, 'author/myarg', 'Correct path' ); 27 | 28 | $count++ == 0 29 | and return; 30 | 31 | return { hello => 'world' }; 32 | }; 33 | } 34 | 35 | my $mc = mcpan(); 36 | can_ok( $mc, '_get' ); 37 | 38 | like( 39 | exception { $mc->_get() }, 40 | qr/^_get takes type and search string as parameters/, 41 | 'Failed with no params', 42 | ); 43 | 44 | like( 45 | exception { $mc->_get('wah') }, 46 | qr/^_get takes type and search string as parameters/, 47 | 'Failed with one param', 48 | ); 49 | 50 | like( 51 | exception { $mc->_get('wah', 'wah', 'wah') }, 52 | qr/^_get takes type and search string as parameters/, 53 | 'Failed with more than two params', 54 | ); 55 | 56 | # call fetch and fail 57 | like( 58 | exception { $mc->_get( 'author', 'myarg' ) }, 59 | qr/^Failed to fetch Author \(myarg\)/, 60 | 'Correct failure', 61 | ); 62 | 63 | # call fetch and succeed 64 | my $res = $mc->_get( 'author', 'myarg' ); 65 | is( $res, 'ok', 'Correct result' ); 66 | 67 | -------------------------------------------------------------------------------- /t/api/_get_or_search.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 10; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | { 12 | no warnings qw; 13 | 14 | *MetaCPAN::Client::_search = sub { 15 | my ( $self, $type, $arg, $params ) = @_; 16 | ::isa_ok( $self, 'MetaCPAN::Client' ); 17 | ::is( $type, 'type', 'Correct type' ); 18 | ::is_deeply( $arg, { hello => 'world' }, 'Correct arg' ); 19 | ::is_deeply( $params, { this => 'that' }, 'Correct params' ); 20 | }; 21 | 22 | *MetaCPAN::Client::_get = sub { 23 | my ( $self, $type, $arg ) = @_; 24 | ::isa_ok( $self, 'MetaCPAN::Client' ); 25 | ::is( $type, 'typeB', 'Correct type in _get' ); 26 | ::is( $arg, 'argb', 'Correct arg in _get' ); 27 | }; 28 | } 29 | 30 | my $mc = mcpan(); 31 | can_ok( $mc, '_get_or_search' ); 32 | 33 | # if arg is hash, it should call _search with it 34 | $mc->_get_or_search( 'type', { hello => 'world' }, { this => 'that' } ); 35 | 36 | # if not, check for arg and call _get 37 | $mc->_get_or_search( 'typeB', 'argb' ); 38 | 39 | # make arg fail check 40 | like( 41 | exception { $mc->_get_or_search( 'type', sub {1} ) }, 42 | qr/^type: invalid args/, 43 | 'Failed execution', 44 | ); 45 | 46 | -------------------------------------------------------------------------------- /t/api/_search.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 19; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | { 12 | no warnings qw; 13 | 14 | my $count = 0; 15 | *MetaCPAN::Client::ssearch = sub { 16 | my ( $self, $type, $args, $params ) = @_; 17 | ::isa_ok( $self, 'MetaCPAN::Client' ); 18 | ::is( $type, 'author', 'Correct type' ); 19 | ::is_deeply( $args, { hello => 'world' }, 'Correct args' ); 20 | 21 | if ( $count++ == 0 ) { 22 | ::is_deeply( $params, {}, 'Correct empty params' ); 23 | } else { 24 | ::is_deeply( $params, { a => 'b' }, 'Correct params' ); 25 | } 26 | 27 | return { a => 'ok' }; 28 | }; 29 | 30 | *MetaCPAN::Client::ResultSet::new = sub { 31 | my ( $self, %args ) = @_; 32 | ::isa_ok( $self, 'MetaCPAN::Client::ResultSet' ); 33 | ::is_deeply( 34 | \%args, 35 | { 36 | scroller => { a => 'ok' }, 37 | type => 'author', 38 | }, 39 | 'Correct args to ::ResultSet', 40 | ); 41 | 42 | return 'yoyo'; 43 | }; 44 | } 45 | 46 | my $mc = mcpan(); 47 | can_ok( $mc, '_search' ); 48 | 49 | like( 50 | exception { $mc->_search('author') }, 51 | qr/^_search takes a hash ref as query/, 52 | 'Failed with no query', 53 | ); 54 | 55 | like( 56 | exception { $mc->_search( 'author', { hello => 'world' }, 'fail' ) }, 57 | qr/^_search takes a hash ref as query parameters/, 58 | 'Failed with no query parameters', 59 | ); 60 | 61 | like( 62 | exception { $mc->_search( 'authorz', { hello => 'world' }, { a => 'b' } ) }, 63 | qr/^search type is not supported/, 64 | 'Unsupported search type', 65 | ); 66 | 67 | is( 68 | $mc->_search( 'author', { hello => 'world' } ), 69 | 'yoyo', 70 | 'Works with no query parameters', 71 | ); 72 | 73 | is( 74 | $mc->_search( 'author', { hello => 'world' }, { a => 'b' } ), 75 | 'yoyo', 76 | 'Correct _search call', 77 | ); 78 | 79 | -------------------------------------------------------------------------------- /t/api/author.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More; 6 | use Test::Fatal; 7 | use Ref::Util qw< is_arrayref >; 8 | 9 | use lib '.'; 10 | use t::lib::Functions; 11 | 12 | my $mc = mcpan(); 13 | can_ok( $mc, 'author' ); 14 | 15 | my $author = $mc->author('XSAWYERX'); 16 | isa_ok( $author, 'MetaCPAN::Client::Author' ); 17 | can_ok( $author, 'pauseid' ); 18 | is( $author->pauseid, 'XSAWYERX', 'Correct author' ); 19 | 20 | my $most_daves; 21 | { 22 | my $daves = $mc->author( { 23 | either => [ 24 | { name => 'Dave *' }, 25 | { name => 'David *' }, 26 | ] 27 | } ); 28 | 29 | isa_ok( $daves, 'MetaCPAN::Client::ResultSet' ); 30 | can_ok( $daves, 'total' ); 31 | ok( $daves->total > 200, 'Lots of Daves' ); 32 | 33 | $most_daves = $daves->total; 34 | } 35 | 36 | { 37 | my $daves = $mc->author( { 38 | either => [ 39 | { name => 'Dave *' }, 40 | { name => 'David *' }, 41 | ], 42 | not => [ 43 | { name => 'Dave S*' }, 44 | { name => 'David S*' }, 45 | ], 46 | } ); 47 | 48 | isa_ok( $daves, 'MetaCPAN::Client::ResultSet' ); 49 | can_ok( $daves, 'total' ); 50 | ok( $daves->total < $most_daves, 'Definitely less Daves' ); 51 | } 52 | 53 | { 54 | my $daves = $mc->author( { 55 | either => [ 56 | { 57 | all => [ 58 | { name => 'Dave *' }, 59 | { email => '*gmail.com' }, 60 | ], 61 | }, 62 | 63 | { 64 | all => [ 65 | { name => 'David *' }, 66 | { email => '*gmail.com' }, 67 | ], 68 | }, 69 | ] 70 | } ); 71 | 72 | isa_ok( $daves, 'MetaCPAN::Client::ResultSet' ); 73 | can_ok( $daves, 'total' ); 74 | ok( $daves->total <= $most_daves, 'Definitely not more Daves' ); 75 | 76 | while ( my $dave = $daves->next ) { 77 | my @emails = is_arrayref $dave->email ? @{ $dave->email } : $dave->email; 78 | ok( 79 | grep( +( $_ =~ /gmail\.com$/ ), @emails ), 80 | 'This Dave has a Gmail account', 81 | ); 82 | } 83 | } 84 | 85 | my $johns = $mc->author( { 86 | all => [ 87 | { name => 'John *' }, 88 | { email => '*gmail.com' }, 89 | ] 90 | } ); 91 | 92 | isa_ok( $johns, 'MetaCPAN::Client::ResultSet' ); 93 | can_ok( $johns, 'total' ); 94 | ok( $johns->total > 0, 'Got some Johns' ); 95 | 96 | while ( my $john = $johns->next ) { 97 | my @emails = is_arrayref $john->email ? @{ $john->email } : $john->email; 98 | ok( 99 | grep( +( $_ =~ /gmail\.com$/ ), @emails ), 100 | 'This John has a Gmail account', 101 | ); 102 | } 103 | 104 | done_testing; 105 | -------------------------------------------------------------------------------- /t/api/cover.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 8; 6 | use Ref::Util qw< is_hashref is_ref >; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'cover' ); 13 | 14 | my $cover = $mc->cover('Moose-2.2007'); 15 | isa_ok( $cover, 'MetaCPAN::Client::Cover' ); 16 | can_ok( $cover, qw< distribution release version criteria > ); 17 | ok( !is_ref($cover->distribution), "distribution is not a ref"); 18 | ok( !is_ref($cover->release), "release is not a ref"); 19 | ok( !is_ref($cover->version), "version is not a ref"); 20 | ok( is_hashref($cover->criteria), "criteria is a hashref"); 21 | -------------------------------------------------------------------------------- /t/api/distribution.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 9; 6 | use Test::Fatal; 7 | use Ref::Util qw< is_hashref >; 8 | 9 | use lib '.'; 10 | use t::lib::Functions; 11 | 12 | my $mc = mcpan(); 13 | can_ok( $mc, 'distribution' ); 14 | 15 | my $dist = $mc->distribution('Business-ISBN'); 16 | isa_ok( $dist, 'MetaCPAN::Client::Distribution' ); 17 | can_ok( $dist, 'name' ); 18 | is( $dist->name, 'Business-ISBN', 'Correct distribution' ); 19 | 20 | can_ok( $dist, 'rt' ); 21 | ok( is_hashref( $dist->rt ), 'rt returns a hashref' ); 22 | 23 | can_ok( $dist, 'github' ); 24 | ok( is_hashref( $dist->github ), 'github returns a hashref' ); 25 | -------------------------------------------------------------------------------- /t/api/download_url.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 17; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'rating' ); 13 | 14 | { 15 | my $rs = $mc->download_url( 'Moose' ); 16 | isa_ok( $rs, 'MetaCPAN::Client::DownloadURL' ); 17 | can_ok( $rs, 'date' ); 18 | can_ok( $rs, 'download_url' ); 19 | can_ok( $rs, 'status' ); 20 | can_ok( $rs, 'version' ); 21 | } 22 | 23 | { 24 | note "request an older version"; 25 | # URL for 1.01 should not change over time, let's check it 26 | my $rs = $mc->download_url( 'Moose', 1.01 ); 27 | isa_ok( $rs, 'MetaCPAN::Client::DownloadURL' ); 28 | is $rs->version(), '1.01'; 29 | is $rs->download_url(), 30 | q[https://cpan.metacpan.org/authors/id/F/FL/FLORA/Moose-1.01.tar.gz], 31 | 'download_url for Moose-1.01'; 32 | is $rs->checksum_sha256(), 33 | q[f4424f4d709907dea8bc9de2a37b9d3fef4f87775a8c102f432c48a1fdf8067b], 34 | 'sha256 for Moose-1.0.1.tar.gz'; 35 | is $rs->checksum_md5(), 36 | q[f13f9c203d099f5dc6117f59bda96340], 37 | 'md5 for Moose-1.0.1.tar.gz'; 38 | } 39 | 40 | { 41 | note "request a range"; 42 | my $rs = $mc->download_url( 'Moose', '>1.01,<=2.00' ); 43 | isa_ok( $rs, 'MetaCPAN::Client::DownloadURL' ); 44 | is $rs->version(), '1.07'; 45 | is $rs->download_url(), 46 | q[https://cpan.metacpan.org/authors/id/F/FL/FLORA/Moose-1.07.tar.gz], 47 | 'download_url for Moose-1.07'; 48 | } 49 | 50 | { 51 | note "request a devel version with range"; 52 | my $rs = $mc->download_url( 'Try::Tiny', '>0.21,<0.27', 1 ); 53 | isa_ok( $rs, 'MetaCPAN::Client::DownloadURL' ); 54 | is $rs->version(), q[0.22], 'Try::Tiny >0.21,<0.27 dev=1'; 55 | } 56 | -------------------------------------------------------------------------------- /t/api/favorite.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 2 + 4 * 2; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'favorite' ); 13 | 14 | foreach my $option ( { author => 'XSAWYERX' }, { dist => 'MetaCPAN-Client' } ) { 15 | my $rs = $mc->favorite($option); 16 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 17 | can_ok( $rs, qw ); 18 | is( $rs->type, 'favorite', 'Correct resultset type' ); 19 | isa_ok( $rs->scroller, 'MetaCPAN::Client::Scroll' ); 20 | } 21 | -------------------------------------------------------------------------------- /t/api/file.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 11; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'file' ); 13 | 14 | my $file = $mc->file('DOY/Moose-2.0001/lib/Moose.pm'); 15 | isa_ok( $file, 'MetaCPAN::Client::File' ); 16 | can_ok( $file, qw ); 17 | is( $file->author, 'DOY', 'Correct author' ); 18 | is( $file->distribution, 'Moose', 'Correct distribution' ); 19 | is( $file->name, 'Moose.pm', 'Correct name' ); 20 | is( $file->path, 'lib/Moose.pm', 'Correct path' ); 21 | is( $file->release, 'Moose-2.0001', 'Correct release' ); 22 | is( $file->version, '2.0001', 'Correct version' ); 23 | 24 | like( $file->source, qr/^\s*package Moose\;/, 'Correct source' ); 25 | 26 | -------------------------------------------------------------------------------- /t/api/module.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 10; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'module' ); 13 | 14 | my $module = $mc->module('MetaCPAN::API'); 15 | isa_ok( $module, 'MetaCPAN::Client::Module' ); 16 | can_ok( $module, qw ); 17 | is( $module->distribution, 'MetaCPAN-API', 'Correct distribution' ); 18 | is( $module->name, 'API.pm', 'Correct name' ); 19 | is( $module->path, 'lib/MetaCPAN/API.pm', 'Correct path' ); 20 | 21 | my $rs = $mc->module( { path => 'lib/MetaCPAN' } ); 22 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 23 | can_ok( $rs, 'total' ); 24 | ok( $rs->total > 0, 'More than a single result in result set' ); 25 | 26 | -------------------------------------------------------------------------------- /t/api/package.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 10; 6 | use Ref::Util qw< is_arrayref is_ref >; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'package' ); 13 | 14 | my $pack = $mc->package('MooseX::Types'); 15 | isa_ok( $pack, 'MetaCPAN::Client::Package' ); 16 | can_ok( $pack, qw< module_name file distribution version author > ); 17 | ok( !is_ref($pack->module_name), "module_name is not a ref"); 18 | ok( !is_ref($pack->file), "file is not a ref"); 19 | ok( !is_ref($pack->distribution), "distribution is not a ref"); 20 | ok( !is_ref($pack->version), "version is not a ref"); 21 | ok( !is_ref($pack->dist_version), "version is not a ref"); 22 | ok( !is_ref($pack->author), "author is not a ref"); 23 | -------------------------------------------------------------------------------- /t/api/permission.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 7; 6 | use Ref::Util qw< is_arrayref is_ref >; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'permission' ); 13 | 14 | my $perm = $mc->permission('MooseX::Types'); 15 | isa_ok( $perm, 'MetaCPAN::Client::Permission' ); 16 | can_ok( $perm, qw< module_name owner co_maintainers > ); 17 | ok( !is_ref($perm->module_name), "module_name is not a ref"); 18 | ok( !is_ref($perm->owner), "owner is not a ref"); 19 | ok( is_arrayref($perm->co_maintainers), "co_maintainers is an arrayref"); 20 | -------------------------------------------------------------------------------- /t/api/pod.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 5; 6 | 7 | use lib '.'; 8 | use t::lib::Functions; 9 | 10 | my $mc = mcpan(); 11 | can_ok( $mc, 'pod' ); 12 | 13 | my $pod = $mc->pod('MetaCPAN::API'); 14 | isa_ok( $pod, 'MetaCPAN::Client::Pod' ); 15 | can_ok( $pod, qw ); 16 | like( $pod->x_pod, qr/=head1/, 'got pod' ); 17 | 18 | -------------------------------------------------------------------------------- /t/api/rating.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 4; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'rating' ); 13 | 14 | my $rs = $mc->rating( { distribution => 'Moose' } ); 15 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 16 | can_ok( $rs, 'next' ); 17 | -------------------------------------------------------------------------------- /t/api/release.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | use Test::More tests => 7; 6 | use Test::Fatal; 7 | 8 | use lib '.'; 9 | use t::lib::Functions; 10 | 11 | my $mc = mcpan(); 12 | can_ok( $mc, 'release' ); 13 | 14 | my $release = $mc->release('MetaCPAN-API'); 15 | isa_ok( $release, 'MetaCPAN::Client::Release' ); 16 | can_ok( $release, qw ); 17 | is( $release->distribution, 'MetaCPAN-API', 'Correct distribution' ); 18 | like($release->checksum_sha256, qr/^[a-f0-9]{64}$/, 'Has a sha256 hexdigest'); 19 | like($release->checksum_md5, qr/^[a-f0-9]{32}$/, 'Has a md5 hexdigest'); 20 | -------------------------------------------------------------------------------- /t/api/reverse-dependencies.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More; 4 | use Test::Fatal; 5 | use Ref::Util qw< is_hashref >; 6 | 7 | use lib '.'; 8 | use t::lib::Functions; 9 | 10 | my $mc = mcpan(); 11 | can_ok( $mc, qw< reverse_dependencies rev_deps > ); 12 | 13 | my $module = 'MetaCPAN::Client'; 14 | 15 | my $rs = $mc->reverse_dependencies($module); 16 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 17 | 18 | my @revdeps; 19 | 20 | while ( my $release = $rs->next ) { 21 | is( 22 | ref $release, 23 | 'MetaCPAN::Client::Release', 24 | 'ResultSet->next with ' . $release->distribution . ' is ok', 25 | ), 26 | 27 | push @revdeps, $release->distribution; 28 | } 29 | 30 | ok( @revdeps > 2, 'revdep count for MetaCPAN::Client seems ok' ); 31 | 32 | foreach my $dep (@revdeps) { 33 | my $ok = eval { 34 | $mc->distribution($dep)->name; 35 | 1; 36 | }; 37 | is( $ok, 1, "$dep is a valid reverse dependency" ); 38 | } 39 | 40 | # Counting here would be fragile since it depends on dependency changes 41 | done_testing(); 42 | -------------------------------------------------------------------------------- /t/entity.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 6; 7 | use Test::Fatal; 8 | 9 | { 10 | package MetaCPAN::Client::FakeEntityEmpty; 11 | use Moo; 12 | with 'MetaCPAN::Client::Role::Entity'; 13 | sub BUILDARGS { 14 | my ( $class, %args ) = @_; 15 | return \%args; 16 | } 17 | } 18 | 19 | { 20 | package MetaCPAN::Client::FakeEntityFull; 21 | use Moo; 22 | with 'MetaCPAN::Client::Role::Entity'; 23 | 24 | sub _known_fields { 25 | +{ 26 | scalar => ['this'], 27 | arrayref => [], 28 | hashref => [], 29 | } 30 | } 31 | } 32 | 33 | ok( 34 | exception { MetaCPAN::Client::FakeEntityEmpty->new }, 35 | 'data is missing, causing exception', 36 | ); 37 | 38 | is( 39 | exception { MetaCPAN::Client::FakeEntityEmpty->new( data => {} ) }, 40 | undef, 41 | 'data available, not causing exception', 42 | ); 43 | 44 | like( 45 | exception { MetaCPAN::Client::FakeEntityEmpty->new_from_request( {} ) }, 46 | qr/.*Can't locate.*_known_fields/, 47 | 'Subroutine _known_fields missing', 48 | ); 49 | 50 | is( 51 | exception { MetaCPAN::Client::FakeEntityFull->new( data => {} ) }, 52 | undef, 53 | 'data available, not causing exception', 54 | ); 55 | 56 | my $fe = MetaCPAN::Client::FakeEntityFull->new_from_request( 57 | { that => 'this', this => 'that' } 58 | ); 59 | 60 | isa_ok( $fe, 'MetaCPAN::Client::FakeEntityFull' ); 61 | is_deeply( $fe->{'data'}, { this => 'that' }, 'Correct data' ); 62 | -------------------------------------------------------------------------------- /t/lib/Functions.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use MetaCPAN::Client; 4 | use Test::More; 5 | 6 | my $version = $MetaCPAN::Client::VERSION || 'xx'; 7 | 8 | sub mcpan { 9 | my $mc = MetaCPAN::Client->new( 10 | ua_args => [ agent => "MetaCPAN::Client-testing/$version" ], 11 | @_, 12 | ); 13 | 14 | isa_ok( $mc, 'MetaCPAN::Client' ); 15 | 16 | return $mc; 17 | } 18 | 19 | 1; 20 | -------------------------------------------------------------------------------- /t/request.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 7; 7 | use MetaCPAN::Client; 8 | use MetaCPAN::Client::Request; 9 | 10 | my $req = MetaCPAN::Client::Request->new( domain => 'https://mydomain' ); 11 | isa_ok( $req, 'MetaCPAN::Client::Request' ); 12 | can_ok( 13 | $req, 14 | qw, 17 | ); 18 | 19 | is( $req->domain, 'https://mydomain', 'Correct domain' ); 20 | is( $req->base_url, 'https://mydomain', 'Correct base_url' ); 21 | isa_ok( $req->ua, 'HTTP::Tiny' ); 22 | 23 | my $ver = $MetaCPAN::Client::VERSION || 'xx'; 24 | is_deeply( 25 | $req->ua_args, 26 | [ agent => "MetaCPAN::Client/$ver", 27 | verify_SSL => 1 ], 28 | 'Correct UA args', 29 | ); 30 | 31 | my $client = MetaCPAN::Client->new( domain => 'foo' ); 32 | is ( $client->request->domain, 'foo', 'domain set in request' ); 33 | -------------------------------------------------------------------------------- /t/result_custom.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 5; 7 | use Test::Fatal qw( exception ); 8 | use MetaCPAN::Client; 9 | use MetaCPAN::Client::ResultSet; 10 | 11 | { 12 | 13 | package Test::Result; 14 | use Moo; 15 | with 'MetaCPAN::Client::Role::Entity'; 16 | 17 | sub new_from_request { 18 | my ( $class, $request, $client ) = @_; 19 | return $class->new( ( defined $client ? ( client => $client ) : () ), 20 | data => $request, ); 21 | } 22 | 23 | sub _known_fields { +{} } 24 | } 25 | 26 | my $client = MetaCPAN::Client->new(); 27 | my $scroll = $client->ssearch( 'author', { pauseid => 'KENTNL' } ); 28 | 29 | my $rs = MetaCPAN::Client::ResultSet->new( 30 | class => 'Test::Result', 31 | scroller => $scroll, 32 | ); 33 | 34 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 35 | can_ok( $rs, qw ); 36 | my $item; 37 | is( exception { $item = $rs->next; 1 }, undef, "no fail on next" ); 38 | isa_ok( $item, 'Test::Result' ); 39 | 40 | my $ex; 41 | isnt( $ex = exception { MetaCPAN::Client::ResultSet->new( scroller => $scroll ) }, 42 | undef, 'Must fail is neither class or type are passed' ); 43 | -------------------------------------------------------------------------------- /t/resultset.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 3; 7 | use Test::Fatal; 8 | use MetaCPAN::Client::ResultSet; 9 | 10 | { 11 | package MetaCPAN::Client::Test::ScrollerZ; 12 | use base 'MetaCPAN::Client::Scroll'; # < 5.10 FTW (except, no) 13 | sub total {0} 14 | } 15 | 16 | like( 17 | exception { 18 | MetaCPAN::Client::ResultSet->new( 19 | type => 'failZZ', 20 | scroller => bless {}, 'MetaCPAN::Client::Test::ScrollerZ', 21 | ) 22 | }, 23 | qr/Invalid type/, 24 | 'Invalid type fail', 25 | ); 26 | 27 | my $rs = MetaCPAN::Client::ResultSet->new( 28 | type => 'author', 29 | scroller => bless {}, 'MetaCPAN::Client::Scroll', 30 | ); 31 | 32 | isa_ok( $rs, 'MetaCPAN::Client::ResultSet' ); 33 | can_ok( $rs, qw ); 34 | -------------------------------------------------------------------------------- /t/scroll.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 6; 7 | use Ref::Util qw< is_hashref >; 8 | use HTTP::Tiny; 9 | use MetaCPAN::Client::Scroll; 10 | use MetaCPAN::Client::Release; 11 | 12 | my $scroller = MetaCPAN::Client::Scroll->new( 13 | ua => HTTP::Tiny->new, 14 | base_url => 'https://fastapi.metacpan.org/v1/', 15 | type => 'release', 16 | body => { query => { term => { distribution => 'MetaCPAN-Client' } } }, 17 | size => 50, 18 | ); 19 | isa_ok( $scroller, 'MetaCPAN::Client::Scroll' ); 20 | 21 | can_ok( 22 | $scroller, 23 | qw< aggregations base_url body _buffer 24 | BUILDARGS DEMOLISH _fetch_next _id 25 | next size time total type ua > 26 | ); 27 | 28 | my $next = $scroller->next; 29 | ok( is_hashref($next), 'next doc returns a hashref' ); 30 | 31 | my $rel = MetaCPAN::Client::Release->new_from_request( $next->{'_source'} ); 32 | isa_ok( $rel, 'MetaCPAN::Client::Release' ); 33 | is( $rel->distribution, 'MetaCPAN-Client', 'release object can be created from next doc' ); 34 | 35 | my $got = 1; # we call ->next once above 36 | while ( my $n = $scroller->next ) { $got++ } 37 | is( $got, $scroller->total, 'can read all matching docs' ); 38 | 39 | 1; 40 | -------------------------------------------------------------------------------- /t/ua_trap.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More; 5 | 6 | # ABSTRACT: Make sure passed value of UA gets used for things. 7 | 8 | use Test::Needs { 9 | 'WWW::Mechanize::Cached' => 1.54, 10 | 'HTTP::Tiny::Mech' => 1.001002, 11 | }; 12 | use Test::Fatal qw( exception ); 13 | 14 | { 15 | 16 | package TrapUA; 17 | our $VERSION = '0.01'; 18 | use Moo; 19 | extends 'HTTP::Tiny::Mech'; 20 | 21 | sub mechua { 22 | require WWW::Mechanize::Cached; 23 | return WWW::Mechanize::Cached->new(); 24 | } 25 | } 26 | 27 | { 28 | require HTTP::Tiny; 29 | no warnings "redefine"; 30 | *HTTP::Tiny::request = sub { 31 | my ( $self, @args ) = @_; 32 | die "Illegal use of HTTP::Tiny" . pp( \@args ); 33 | }; 34 | } 35 | use MetaCPAN::Client; 36 | 37 | my $e; 38 | is( 39 | $e = exception { 40 | my $client = MetaCPAN::Client->new( ua => TrapUA->new() ); 41 | 42 | my $a = $client->author('KENTNL'); 43 | my $releases = $a->releases; 44 | }, 45 | undef, 46 | "No illegal methods called" 47 | ); 48 | 49 | if ($e) { diag explain $e } 50 | 51 | done_testing; 52 | 53 | --------------------------------------------------------------------------------