From be8a4b8b44782bb05e84014c70a3b43e8895a13c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 27 Jun 2025 10:07:48 +0200 Subject: [PATCH 1/5] Add linecount data to API responses and update OpenAPI spec This extends the API serialization to include linecount statistics in version responses by adding a `linecounts` field to the `EncodableVersion` struct and updating the OpenAPI specification to document the new field. All API response test snapshots across crates and versions endpoints are updated to include the new linecount data. The implementation maintains backward compatibility with `null` linecount values for existing data. --- ...rates_io__openapi__tests__openapi_snapshot-2.snap | 7 ++++++- ..._krate__publish__edition__edition_is_saved-4.snap | 5 +++++ ...te__publish__links__crate_with_links_field-3.snap | 5 +++++ ...__krate__publish__manifest__boolean_readme-4.snap | 5 +++++ ...rate__publish__manifest__lib_and_bin_crate-4.snap | 11 +++++++++++ ...tests__krate__publish__trustpub__full_flow-9.snap | 5 +++++ ..._krate__yanking__patch_version_yank_unyank-2.snap | 7 ++++++- ..._krate__yanking__patch_version_yank_unyank-3.snap | 7 ++++++- ..._krate__yanking__patch_version_yank_unyank-4.snap | 7 ++++++- ..._krate__yanking__patch_version_yank_unyank-5.snap | 7 ++++++- ..._krate__yanking__patch_version_yank_unyank-6.snap | 7 ++++++- ...s__krate__yanking__patch_version_yank_unyank.snap | 7 ++++++- ..._routes__crates__admin__index_include_yanked.snap | 1 - ...tes__crates__read__include_default_version-2.snap | 1 + ...ates_io__tests__routes__crates__read__show-2.snap | 3 +++ ...sts__routes__crates__read__show_all_yanked-2.snap | 2 ++ ...sions_not_included_in_reverse_dependencies-2.snap | 1 + ...reverse_dependencies__reverse_dependencies-2.snap | 1 + ...es_includes_published_by_user_when_present-2.snap | 2 ++ ...es_query_supports_u64_version_number_parts-2.snap | 1 + ...hen_old_version_doesnt_depend_but_new_does-2.snap | 1 + ...sions_not_included_in_reverse_dependencies-2.snap | 1 + ...__routes__crates__versions__list__versions-2.snap | 3 +++ ...how_by_crate_name_and_semver_no_published_by.snap | 1 + ...rsions__read__show_by_crate_name_and_version.snap | 1 + src/views.rs | 12 ++++++++++++ 26 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap index 115f1d84123..f24f7d0488d 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap @@ -1108,6 +1108,10 @@ expression: response.json() "null" ] }, + "linecounts": { + "description": "Line count statistics for this version.\n\nStatus: **Unstable**\n\nThis field may be `null` until the version has been analyzed, which\nhappens in an asynchronous background job.", + "type": "object" + }, "links": { "$ref": "#/components/schemas/VersionLinks", "description": "Links to other API endpoints related to this version." @@ -1190,7 +1194,8 @@ expression: response.json() "links", "crate_size", "audit_actions", - "checksum" + "checksum", + "linecounts" ], "type": "object" }, diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap index 3850557bc57..a96debf1df8 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap index 8f18952f4ec..84e59b00384 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": "git2", "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap index 5fe5f2e0f12..e2e20b682d4 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap index 606721884a8..a1efb78dcc5 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap @@ -36,6 +36,17 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": { + "Rust": { + "code_lines": 3, + "comment_lines": 0, + "files": 3 + } + }, + "total_code_lines": 3, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap index 1ad21be16e3..ea0ac3dfa2f 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap @@ -21,6 +21,11 @@ expression: response.json() "id": 2, "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.1.0/authors", "dependencies": "/api/v1/crates/foo/1.1.0/dependencies", diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap index 9782ad19f8e..a4620b61f50 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap @@ -63,6 +63,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap index 3024140b782..9fa9757feab 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap @@ -74,6 +74,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap index 3024140b782..9fa9757feab 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap @@ -74,6 +74,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap index 8e3a9965e2a..4dcd1c8d08f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap @@ -85,6 +85,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap index 8e3a9965e2a..4dcd1c8d08f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap @@ -85,6 +85,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap index 9782ad19f8e..a4620b61f50 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap @@ -63,6 +63,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap index 6bc1c1257cb..174c12ac434 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap @@ -1,7 +1,6 @@ --- source: src/tests/routes/crates/admin.rs expression: response.json() -snapshot_kind: text --- { "crates": [ diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap index c5d5669a769..85a9950f33f 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap @@ -55,6 +55,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_default_version/0.5.1/authors", "dependencies": "/api/v1/crates/foo_default_version/0.5.1/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap index 1db5c8d3bcc..0d8f97403d5 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap @@ -68,6 +68,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.1/authors", "dependencies": "/api/v1/crates/foo_show/0.5.1/dependencies", @@ -107,6 +108,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.0/authors", "dependencies": "/api/v1/crates/foo_show/0.5.0/dependencies", @@ -146,6 +148,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/1.0.0/authors", "dependencies": "/api/v1/crates/foo_show/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap index b24044455e9..b18dbcdc4b9 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap @@ -67,6 +67,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.0/authors", "dependencies": "/api/v1/crates/foo_show/0.5.0/dependencies", @@ -106,6 +107,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/1.0.0/authors", "dependencies": "/api/v1/crates/foo_show/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap index a28a59217da..0100fa322c8 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c3/1.0.0/authors", "dependencies": "/api/v1/crates/c3/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap index 5a6e2f0bc53..cf5c91caf66 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/1.1.0/authors", "dependencies": "/api/v1/crates/c2/1.1.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap index 95e0eefb230..f189d3c5896 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap @@ -51,6 +51,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c3/3.0.0/authors", "dependencies": "/api/v1/crates/c3/3.0.0/dependencies", @@ -90,6 +91,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap index 2f07702d00c..4c6614b2420 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/1.0.18446744073709551615/authors", "dependencies": "/api/v1/crates/c2/1.0.18446744073709551615/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap index 0b9b5db1cf4..e0c4e01afe0 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap index 0b9b5db1cf4..e0c4e01afe0 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap index 92ea2d62e20..521f406a88c 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap @@ -26,6 +26,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/1.0.0/authors", "dependencies": "/api/v1/crates/foo_versions/1.0.0/dependencies", @@ -59,6 +60,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/0.5.1/authors", "dependencies": "/api/v1/crates/foo_versions/0.5.1/dependencies", @@ -98,6 +100,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/0.5.0/authors", "dependencies": "/api/v1/crates/foo_versions/0.5.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap index 3e9fd9dbe94..21df33f67dd 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap @@ -21,6 +21,7 @@ expression: json "id": "[id]", "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_vers_show_no_pb/1.0.0/authors", "dependencies": "/api/v1/crates/foo_vers_show_no_pb/1.0.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap index f174100a440..603951ed766 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap @@ -21,6 +21,7 @@ expression: json "id": "[id]", "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_vers_show/2.0.0/authors", "dependencies": "/api/v1/crates/foo_vers_show/2.0.0/dependencies", diff --git a/src/views.rs b/src/views.rs index 481e3ec50f5..dc9095b289d 100644 --- a/src/views.rs +++ b/src/views.rs @@ -924,6 +924,15 @@ pub struct EncodableVersion { /// inside it. #[schema(value_type = Option)] pub trustpub_data: Option, + + /// Line count statistics for this version. + /// + /// Status: **Unstable** + /// + /// This field may be `null` until the version has been analyzed, which + /// happens in an asynchronous background job. + #[schema(value_type = Object)] + pub linecounts: Option, } impl EncodableVersion { @@ -955,6 +964,7 @@ impl EncodableVersion { documentation, repository, trustpub_data, + linecounts, .. } = version; @@ -990,6 +1000,7 @@ impl EncodableVersion { documentation, repository, trustpub_data, + linecounts, published_by: published_by.map(User::into), audit_actions: audit_actions .into_iter() @@ -1134,6 +1145,7 @@ mod tests { .and_utc(), }], trustpub_data: None, + linecounts: None, }; let json = serde_json::to_string(&ver).unwrap(); assert_some!(json.as_str().find(r#""updated_at":"2017-01-06T14:23:11Z""#)); From 18a95aebe5825387f972e43215154a044d7fd944 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 27 Jun 2025 10:08:40 +0200 Subject: [PATCH 2/5] Add MSW test support for linecount data This updates the Mock Service Worker test infrastructure to include linecount data by extending the version model with a `linecounts` field and mock data generation. All MSW handler tests are updated to include linecount in API responses, providing realistic mock linecount data for frontend testing. --- .../handlers/crates/downloads.test.js | 37 +++++++++ .../crates-io-msw/handlers/crates/get.test.js | 80 +++++++++++++++++++ .../crates/reverse-dependencies.test.js | 37 +++++++++ .../handlers/versions/follow-updates.test.js | 16 ++++ .../handlers/versions/get.test.js | 16 ++++ .../handlers/versions/list.test.js | 48 +++++++++++ .../handlers/versions/patch.test.js | 32 ++++++++ .../crates-io-msw/models/dependency.test.js | 16 ++++ .../models/version-download.test.js | 16 ++++ packages/crates-io-msw/models/version.js | 47 +++++++++++ packages/crates-io-msw/models/version.test.js | 16 ++++ 11 files changed, 361 insertions(+) diff --git a/packages/crates-io-msw/handlers/crates/downloads.test.js b/packages/crates-io-msw/handlers/crates/downloads.test.js index e588f787de7..2337366adc1 100644 --- a/packages/crates-io-msw/handlers/crates/downloads.test.js +++ b/packages/crates-io-msw/handlers/crates/downloads.test.js @@ -91,6 +91,22 @@ test('includes related versions', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', @@ -113,6 +129,27 @@ test('includes related versions', async function () { features: {}, id: 2, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.0.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.1/downloads', diff --git a/packages/crates-io-msw/handlers/crates/get.test.js b/packages/crates-io-msw/handlers/crates/get.test.js index 2dfa077b331..644a614e5b8 100644 --- a/packages/crates-io-msw/handlers/crates/get.test.js +++ b/packages/crates-io-msw/handlers/crates/get.test.js @@ -56,6 +56,22 @@ test('returns a crate object for known crates', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', @@ -121,6 +137,22 @@ test('works for non-canonical names', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads', @@ -159,6 +191,17 @@ test('includes related versions', async function () { downloads: 11_106, features: {}, license: 'MIT/Apache-2.0', + linecounts: { + languages: { + Python: { + code_lines: 421, + comment_lines: 64, + files: 8, + }, + }, + total_code_lines: 421, + total_comment_lines: 64, + }, links: { dependencies: '/api/v1/crates/rand/1.2.0/dependencies', version_downloads: '/api/v1/crates/rand/1.2.0/downloads', @@ -181,6 +224,27 @@ test('includes related versions', async function () { downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.1.0/dependencies', version_downloads: '/api/v1/crates/rand/1.1.0/downloads', @@ -203,6 +267,22 @@ test('includes related versions', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js index 88c6ec1642d..57a3eac95d1 100644 --- a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js +++ b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js @@ -76,6 +76,27 @@ test('returns a paginated list of crate versions depending to the specified crat downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/baz/1.0.1/dependencies', version_downloads: '/api/v1/crates/baz/1.0.1/downloads', @@ -98,6 +119,22 @@ test('returns a paginated list of crate versions depending to the specified crat downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/bar/1.0.0/dependencies', version_downloads: '/api/v1/crates/bar/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/versions/follow-updates.test.js b/packages/crates-io-msw/handlers/versions/follow-updates.test.js index 49d4ff3c9b6..614aa27adf4 100644 --- a/packages/crates-io-msw/handlers/versions/follow-updates.test.js +++ b/packages/crates-io-msw/handlers/versions/follow-updates.test.js @@ -33,6 +33,22 @@ test('returns latest versions of followed crates', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.2.3/dependencies', version_downloads: '/api/v1/crates/foo/1.2.3/downloads', diff --git a/packages/crates-io-msw/handlers/versions/get.test.js b/packages/crates-io-msw/handlers/versions/get.test.js index 5a37d3eb422..9052552a345 100644 --- a/packages/crates-io-msw/handlers/versions/get.test.js +++ b/packages/crates-io-msw/handlers/versions/get.test.js @@ -34,6 +34,22 @@ test('returns a version object for known version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js index 01b733d6bf7..9787cc239f5 100644 --- a/packages/crates-io-msw/handlers/versions/list.test.js +++ b/packages/crates-io-msw/handlers/versions/list.test.js @@ -39,6 +39,17 @@ test('returns all versions belonging to the specified crate', async function () downloads: 11_106, features: {}, license: 'MIT/Apache-2.0', + linecounts: { + languages: { + Python: { + code_lines: 421, + comment_lines: 64, + files: 8, + }, + }, + total_code_lines: 421, + total_comment_lines: 64, + }, links: { dependencies: '/api/v1/crates/rand/1.2.0/dependencies', version_downloads: '/api/v1/crates/rand/1.2.0/downloads', @@ -61,6 +72,27 @@ test('returns all versions belonging to the specified crate', async function () downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.1.0/dependencies', version_downloads: '/api/v1/crates/rand/1.1.0/downloads', @@ -89,6 +121,22 @@ test('returns all versions belonging to the specified crate', async function () downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/versions/patch.test.js b/packages/crates-io-msw/handlers/versions/patch.test.js index 829c29c16e8..c2b5531fd6e 100644 --- a/packages/crates-io-msw/handlers/versions/patch.test.js +++ b/packages/crates-io-msw/handlers/versions/patch.test.js @@ -64,6 +64,22 @@ test('yanks the version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.0.0/dependencies', version_downloads: '/api/v1/crates/foo/1.0.0/downloads', @@ -95,6 +111,22 @@ test('yanks the version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.0.0/dependencies', version_downloads: '/api/v1/crates/foo/1.0.0/downloads', diff --git a/packages/crates-io-msw/models/dependency.test.js b/packages/crates-io-msw/models/dependency.test.js index b9ad66d9b8c..ae9bc6adfcb 100644 --- a/packages/crates-io-msw/models/dependency.test.js +++ b/packages/crates-io-msw/models/dependency.test.js @@ -72,6 +72,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, diff --git a/packages/crates-io-msw/models/version-download.test.js b/packages/crates-io-msw/models/version-download.test.js index 0dd48be6bca..405b1878682 100644 --- a/packages/crates-io-msw/models/version-download.test.js +++ b/packages/crates-io-msw/models/version-download.test.js @@ -42,6 +42,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, diff --git a/packages/crates-io-msw/models/version.js b/packages/crates-io-msw/models/version.js index bf0b4ec22f9..54801e67698 100644 --- a/packages/crates-io-msw/models/version.js +++ b/packages/crates-io-msw/models/version.js @@ -4,6 +4,8 @@ import { applyDefault } from '../utils/defaults.js'; const LICENSES = ['MIT/Apache-2.0', 'MIT', 'Apache-2.0']; +const LANGUAGES = ['Rust', 'JavaScript', 'TypeScript', 'Python', 'CSS', 'HTML', 'Shell']; + export default { id: primaryKey(Number), @@ -19,6 +21,7 @@ export default { readme: nullable(String), rust_version: nullable(String), trustpub_data: nullable(Object), + linecounts: nullable(Object), crate: oneOf('crate'), publishedBy: nullable(oneOf('user')), @@ -36,9 +39,53 @@ export default { applyDefault(attrs, 'readme', () => null); applyDefault(attrs, 'rust_version', () => null); applyDefault(attrs, 'trustpub_data', () => null); + applyDefault(attrs, 'linecounts', () => generateLinecounts(attrs.id)); if (!attrs.crate) { throw new Error(`Missing \`crate\` relationship on \`version:${attrs.num}\``); } }, }; + +function generateLinecounts(id) { + // Some versions don't have linecount data (simulating older versions) + if (id % 4 === 0) { + return null; + } + + const languages = {}; + let totalCodeLines = 0; + let totalCommentLines = 0; + + // Generate 1-3 random languages per version + const numLanguages = (id % 3) + 1; + const selectedLanguages = []; + + for (let i = 0; i < numLanguages; i++) { + const langIndex = (id + i) % LANGUAGES.length; + selectedLanguages.push(LANGUAGES[langIndex]); + } + + for (const language of selectedLanguages) { + // Generate pseudo-random but deterministic line counts based on id and language + const seed = id + language.codePointAt(0); + const codeLines = ((seed * 137) % 500) + 50; // 50-550 lines + const commentLines = ((seed * 73) % 100) + 5; // 5-105 lines + const files = ((seed * 29) % 8) + 1; // 1-8 files + + languages[language] = { + code_lines: codeLines, + comment_lines: commentLines, + files: files, + }; + + totalCodeLines += codeLines; + totalCommentLines += commentLines; + } + + return { + languages, + total_code_lines: totalCodeLines, + total_comment_lines: totalCommentLines, + }; +} diff --git a/packages/crates-io-msw/models/version.test.js b/packages/crates-io-msw/models/version.test.js index 8d19e77f28d..9006f703c14 100644 --- a/packages/crates-io-msw/models/version.test.js +++ b/packages/crates-io-msw/models/version.test.js @@ -37,6 +37,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, From 80ecdd05a54824f97a8ef574b746f889d33d7711 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 22 Sep 2025 15:32:30 +0200 Subject: [PATCH 3/5] services/intl: Add optional `options` parameter to `formatNumber()` fn --- app/services/intl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/intl.js b/app/services/intl.js index b9b80c7b3a9..94ed0db0704 100644 --- a/app/services/intl.js +++ b/app/services/intl.js @@ -4,7 +4,7 @@ export default class IntlService extends Service { // `undefined` means "use the default language of the browser" locale = undefined; - formatNumber(value) { - return Number(value).toLocaleString(this.locale); + formatNumber(value, options) { + return Number(value).toLocaleString(this.locale, options); } } From 7c02877be03f68694f5168ef8a992458dfc61e15 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 22 Sep 2025 15:40:38 +0200 Subject: [PATCH 4/5] Implement `formatShortNum` helper --- app/helpers/format-short-num.js | 41 +++++++++++++++++ tests/unit/helpers/format-short-num-test.gjs | 48 ++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 app/helpers/format-short-num.js create mode 100644 tests/unit/helpers/format-short-num-test.gjs diff --git a/app/helpers/format-short-num.js b/app/helpers/format-short-num.js new file mode 100644 index 00000000000..b74d3d85e3b --- /dev/null +++ b/app/helpers/format-short-num.js @@ -0,0 +1,41 @@ +import Helper from '@ember/component/helper'; +import { service } from '@ember/service'; + +/** + * This matches the implementation in https://github.com/rust-lang/crates_io_og_image/blob/v0.2.1/src/formatting.rs + * to ensure that we render roughly the same values in our user interface and the generated OpenGraph images. + */ +export default class FormatShortNumHelper extends Helper { + @service intl; + + compute([value]) { + const THRESHOLD = 1500; + const UNITS = ['', 'K', 'M']; + + let numValue = Number(value); + let unitIndex = 0; + + // Keep dividing by 1000 until value is below threshold or we've reached the last unit + while (numValue >= THRESHOLD && unitIndex < UNITS.length - 1) { + numValue /= 1000; + unitIndex += 1; + } + + let unit = UNITS[unitIndex]; + + // Special case for numbers without suffix - no decimal places + if (unitIndex === 0) { + return this.intl.formatNumber(value); + } + + // For K and M, format with appropriate decimal places + // Determine number of decimal places to keep number under 4 chars + let fractionDigits = numValue < 10 ? 1 : 0; + let number = this.intl.formatNumber(numValue, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + }); + + return number + unit; + } +} diff --git a/tests/unit/helpers/format-short-num-test.gjs b/tests/unit/helpers/format-short-num-test.gjs new file mode 100644 index 00000000000..c28d9476998 --- /dev/null +++ b/tests/unit/helpers/format-short-num-test.gjs @@ -0,0 +1,48 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import formatShortNum from 'crates-io/helpers/format-short-num'; +import { setupRenderingTest } from 'crates-io/tests/helpers'; + +module('Unit | Helper | format-short-num', function (hooks) { + setupRenderingTest(hooks); + + async function check(assert, input, expected) { + await render(); + assert.dom().hasText(expected); + } + + test('formats numbers without suffix (below 1500)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 0, '0'); + await check(assert, 1, '1'); + await check(assert, 1000, '1,000'); + await check(assert, 1499, '1,499'); + }); + + test('formats numbers with K suffix (1500 to 1500000)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 1500, '1.5K'); + await check(assert, 2000, '2.0K'); + await check(assert, 5000, '5.0K'); + await check(assert, 10_000, '10K'); + await check(assert, 50_000, '50K'); + await check(assert, 100_000, '100K'); + await check(assert, 500_000, '500K'); + await check(assert, 999_999, '1,000K'); + }); + + test('formats numbers with M suffix (above 1500000)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 1_500_000, '1.5M'); + await check(assert, 2_000_000, '2.0M'); + await check(assert, 5_000_000, '5.0M'); + await check(assert, 10_000_000, '10M'); + await check(assert, 50_000_000, '50M'); + await check(assert, 100_000_000, '100M'); + await check(assert, 1_000_000_000, '1,000M'); + }); +}); From 50bac71ff8cb9ad1828f38935f7fb6a4179d0988 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 27 Jun 2025 10:09:03 +0200 Subject: [PATCH 5/5] Add frontend linecount display This implements linecount display in the crate sidebar by adding a `linecounts` attribute to the `Version` Ember Data model and displaying total code lines alongside existing crate size information. A code icon is included for visual consistency with other metrics. The implementation handles `null` linecount data gracefully for backward compatibility. --- app/components/crate-sidebar.css | 5 ++++- app/components/crate-sidebar.gjs | 15 +++++++++++++++ app/models/version.js | 1 + e2e/acceptance/crate.spec.ts | 12 ++++++++++++ public/assets/code.svg | 5 +++++ tests/acceptance/crate-test.js | 14 ++++++++++++++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 public/assets/code.svg diff --git a/app/components/crate-sidebar.css b/app/components/crate-sidebar.css index fed5290a268..57978a25b77 100644 --- a/app/components/crate-sidebar.css +++ b/app/components/crate-sidebar.css @@ -22,6 +22,7 @@ .msrv, .edition, .license, +.linecount, .bytes, .purl { display: flex; @@ -37,7 +38,8 @@ .date, .msrv, -.edition { +.edition, +.linecount { > span { cursor: help; } @@ -49,6 +51,7 @@ } } +.linecount, .bytes { font-variant-numeric: tabular-nums; } diff --git a/app/components/crate-sidebar.gjs b/app/components/crate-sidebar.gjs index 8ab8277a038..eb32fc3bae6 100644 --- a/app/components/crate-sidebar.gjs +++ b/app/components/crate-sidebar.gjs @@ -21,6 +21,7 @@ import Tooltip from 'crates-io/components/tooltip'; import dateFormat from 'crates-io/helpers/date-format'; import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; import dateFormatIso from 'crates-io/helpers/date-format-iso'; +import formatShortNum from 'crates-io/helpers/format-short-num'; import prettyBytes from 'crates-io/helpers/pretty-bytes'; import { simplifyUrl } from './crate-sidebar/link'; @@ -125,6 +126,20 @@ export default class CrateSidebar extends Component { {{/if}} + {{#if @version.linecounts.total_code_lines}} +
+ {{svgJar 'code'}} + + {{formatShortNum @version.linecounts.total_code_lines}} + SLoC + + Source Lines of Code
+ (excluding comments, integration tests and example code) +
+
+
+ {{/if}} + {{#if @version.crate_size}}
{{svgJar 'weight'}} diff --git a/app/models/version.js b/app/models/version.js index 83c8cf0bf79..6d57a367f7e 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -25,6 +25,7 @@ export default class Version extends Model { @attr yanked; @attr license; @attr crate_size; + @attr linecounts; /** * The minimum supported Rust version of this crate version. diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts index 3d18151c0a7..59daa2badf6 100644 --- a/e2e/acceptance/crate.spec.ts +++ b/e2e/acceptance/crate.spec.ts @@ -210,6 +210,18 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-license]')).toHaveText('MIT OR Apache-2.0'); }); + test('sidebar shows correct information', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '0.5.0' }); + msw.db.version.create({ crate, num: '1.0.0' }); + + await page.goto('/crates/foo'); + await expect(page.locator('[data-test-linecounts]')).toHaveText('1,119 SLoC'); + + await page.goto('/crates/foo/0.5.0'); + await expect(page.locator('[data-test-linecounts]')).toHaveText('520 SLoC'); + }); + test.skip('crates can be yanked by owner', async ({ page, msw }) => { loadFixtures(msw.db); diff --git a/public/assets/code.svg b/public/assets/code.svg new file mode 100644 index 00000000000..3dfae3ce114 --- /dev/null +++ b/public/assets/code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index 5c5be2847f8..d2952da2678 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -306,4 +306,18 @@ module('Acceptance | crate page', function (hooks) { assert.strictEqual(currentURL(), '/crates/nanomsg'); assert.dom('[data-test-keyword]').exists(); }); + + test('sidebar shows correct information', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '0.5.0' }); + this.db.version.create({ crate, num: '1.0.0' }); + + await visit('/crates/foo'); + assert.dom('[data-test-linecounts]').hasText('1,119 SLoC'); + + await visit('/crates/foo/0.5.0'); + assert.dom('[data-test-linecounts]').hasText('520 SLoC'); + }); });