-
Notifications
You must be signed in to change notification settings - Fork 197
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into alestiago/docs-test-arguement-terminator
- Loading branch information
Showing
8 changed files
with
693 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/// Enables checking a package's license from pub.dev. | ||
/// | ||
/// This library is intented to be used by Very Good CLI to help extracting | ||
/// license information. The existance of this library is likely to be | ||
/// ephemeral. It may be obsolete once [pub.dev](https://pub.dev/) exposes | ||
/// stable license information in their official API; you may track the | ||
/// progress [here](https://github.com/dart-lang/pub-dev/issues/4717). | ||
library pub_license; | ||
|
||
import 'package:html/dom.dart' as html_dom; | ||
import 'package:html/parser.dart' as html_parser; | ||
import 'package:http/http.dart' as http; | ||
import 'package:meta/meta.dart'; | ||
|
||
/// The pub.dev [Uri] used to retrieve the license of a package. | ||
Uri _pubPackageLicenseUri(String packageName) => | ||
Uri.parse('https://pub.dev/packages/$packageName/license'); | ||
|
||
/// {@template pub_license_exception} | ||
/// An exception thrown by [PubLicense]. | ||
/// {@endtemplate} | ||
class PubLicenseException implements Exception { | ||
/// {@macro pub_license_exception} | ||
const PubLicenseException(String message) | ||
: message = '[pub_license] $message'; | ||
|
||
/// The exception message. | ||
final String message; | ||
} | ||
|
||
/// The function signature for parsing HTML documents. | ||
@visibleForTesting | ||
typedef HtmlDocumentParse = html_dom.Document Function( | ||
dynamic input, { | ||
String? encoding, | ||
bool generateSpans, | ||
String? sourceUrl, | ||
}); | ||
|
||
/// {@template pub_license} | ||
/// Enables checking pub.dev's hosted packages license. | ||
/// {@endtemplate} | ||
class PubLicense { | ||
/// {@macro pub_license} | ||
PubLicense({ | ||
@visibleForTesting http.Client? client, | ||
@visibleForTesting HtmlDocumentParse? parse, | ||
}) : _client = client ?? http.Client(), | ||
_parse = parse ?? html_parser.parse; | ||
|
||
final http.Client _client; | ||
|
||
final html_dom.Document Function( | ||
dynamic input, { | ||
String? encoding, | ||
bool generateSpans, | ||
String? sourceUrl, | ||
}) _parse; | ||
|
||
/// Retrieves the license of a package. | ||
/// | ||
/// Some packages may have multiple licenses, hence a [Set] is returned. | ||
/// | ||
/// It may throw a [PubLicenseException] if: | ||
/// * The response from pub.dev is not successful. | ||
/// * The response body cannot be parsed. | ||
Future<Set<String>> getLicense(String packageName) async { | ||
final response = await _client.get(_pubPackageLicenseUri(packageName)); | ||
|
||
if (response.statusCode != 200) { | ||
throw PubLicenseException( | ||
'''Failed to retrieve the license of the package, received status code: ${response.statusCode}''', | ||
); | ||
} | ||
|
||
late final html_dom.Document document; | ||
try { | ||
document = _parse(response.body); | ||
} on html_parser.ParseError catch (e) { | ||
throw PubLicenseException( | ||
'Failed to parse the response body, received error: $e', | ||
); | ||
} catch (e) { | ||
throw PubLicenseException( | ||
'''An unknown error occurred when trying to parse the response body, received error: $e''', | ||
); | ||
} | ||
|
||
return _scrapeLicense(document); | ||
} | ||
} | ||
|
||
/// Scrapes the license from the pub.dev's package license page. | ||
/// | ||
/// The expected HTML structure is: | ||
/// ```html | ||
/// <aside class="detail-info-box"> | ||
/// <h3> ... </h3> | ||
/// <p> ... </p> | ||
/// <h3 class="title">License</h3> | ||
/// <p> | ||
/// <img/> | ||
/// MIT (<a href="/packages/very_good_cli/license">LICENSE</a>) | ||
/// </p> | ||
/// </aside> | ||
/// ``` | ||
/// | ||
/// It may throw a [PubLicenseException] if: | ||
/// * The detail info box is not found. | ||
/// * The license header is not found. | ||
Set<String> _scrapeLicense(html_dom.Document document) { | ||
final detailInfoBox = document.querySelector('.detail-info-box'); | ||
if (detailInfoBox == null) { | ||
throw const PubLicenseException( | ||
'''Failed to scrape license because `.detail-info-box` was not found.''', | ||
); | ||
} | ||
|
||
String? rawLicenseText; | ||
for (var i = 0; i < detailInfoBox.children.length; i++) { | ||
final child = detailInfoBox.children[i]; | ||
|
||
final headerText = child.text.trim().toLowerCase(); | ||
if (headerText == 'license') { | ||
rawLicenseText = detailInfoBox.children[i + 1].text.trim(); | ||
break; | ||
} | ||
} | ||
if (rawLicenseText == null) { | ||
throw const PubLicenseException( | ||
'''Failed to scrape license because the license header was not found.''', | ||
); | ||
} | ||
|
||
final licenseText = rawLicenseText.split('(').first.trim(); | ||
return licenseText.split(',').map((e) => e.trim()).toSet(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
test/src/pub_license/fixtures/generate_pub_license_fixtures.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/// A small script used to generate the fixture for the pub_license test. | ||
/// | ||
/// Fixtures are simply a temporary snapshot of an HTML response from pub.dev. | ||
/// The generated fixtures allow testing pub_license scraping logic without | ||
/// making a request to pub.dev every time the test is run. | ||
/// | ||
/// To run this script, use the following command: | ||
/// ```bash | ||
/// dart test/src/pub_license/fixtures/generate_pub_license_fixtures.dart | ||
/// ``` | ||
/// | ||
/// Or simply use the "Run" CodeLens from VSCode's Dart extension if running | ||
/// from VSCode. | ||
library generate_pub_license_fixtures; | ||
|
||
// ignore_for_file: avoid_print | ||
|
||
import 'dart:io'; | ||
|
||
import 'package:http/http.dart' as http; | ||
import 'package:path/path.dart' as path; | ||
|
||
/// [Uri] used to test the case where a package has a single license. | ||
final _singleLicenseUri = Uri.parse( | ||
'https://pub.dev/packages/very_good_cli/license', | ||
); | ||
|
||
/// [Uri] used to test the case where a package has multiple licenses. | ||
final _multipleLicenseUri = Uri.parse( | ||
'https://pub.dev/packages/just_audio/license', | ||
); | ||
|
||
/// [Uri] used to test the case where a package has no license. | ||
final _noLicenseUri = Uri.parse( | ||
'https://pub.dev/packages/music_control_notification/license', | ||
); | ||
|
||
Future<void> main() async { | ||
final fixtureUris = <String, Uri>{ | ||
'singleLicense': _singleLicenseUri, | ||
'multipleLicense': _multipleLicenseUri, | ||
'noLicense': _noLicenseUri, | ||
}; | ||
|
||
final httpClient = http.Client(); | ||
|
||
for (final entry in fixtureUris.entries) { | ||
final name = entry.key; | ||
final uri = entry.value; | ||
|
||
final response = await httpClient.get(uri); | ||
|
||
if (response.statusCode != 200) { | ||
print( | ||
'''Failed to generate a fixture for $name, received status code: ${response.statusCode}''', | ||
); | ||
continue; | ||
} | ||
|
||
final fixturePath = path.joinAll([ | ||
Directory.current.path, | ||
'test', | ||
'src', | ||
'pub_license', | ||
'fixtures', | ||
'$name.html', | ||
]); | ||
File(fixturePath) | ||
..createSync(recursive: true) | ||
..writeAsStringSync(response.body); | ||
|
||
print('Fixture generated at $fixturePath'); | ||
} | ||
} |
Oops, something went wrong.