feat(stats): use JSON rows

This commit is contained in:
Elijah Potter 2025-03-26 12:24:08 -06:00
parent 264c2c23a8
commit f96a06eaed
7 changed files with 105 additions and 35 deletions

25
Cargo.lock generated
View file

@ -399,27 +399,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "csv"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "darling"
version = "0.20.10"
@ -837,9 +816,11 @@ dependencies = [
name = "harper-stats"
version = "0.25.0"
dependencies = [
"csv",
"harper-core",
"quickcheck",
"quickcheck_macros",
"serde",
"serde_json",
"uuid",
]

View file

@ -207,7 +207,7 @@ fn main() -> anyhow::Result<()> {
Args::SummarizeLintRecord { file } => {
let file = File::open(file)?;
let mut reader = BufReader::new(file);
let stats = Stats::read_csv(&mut reader)?;
let stats = Stats::read(&mut reader)?;
let summary = stats.summarize();
println!("{summary}");

View file

@ -123,7 +123,7 @@ impl Backend {
.create(true)
.open(&config.stats_path)?,
);
stats.write_csv(&mut writer)?;
stats.write(&mut writer)?;
writer.flush()?;
Ok(())

View file

@ -7,4 +7,8 @@ edition = "2021"
serde = { version = "1.0.217", features = ["derive"] }
harper-core = { path = "../harper-core", version = "0.25.0", features = ["concurrent"] }
uuid = { version = "1.12.0", features = ["serde", "v4"] }
csv = "1.3.1"
serde_json = "1.0.140"
[dev-dependencies]
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"

3
harper-stats/README.md Normal file
View file

@ -0,0 +1,3 @@
# `harper-stats`
This crate contains the centralized logic for Harper's statistics logging.

View file

@ -2,12 +2,16 @@ mod record;
mod summary;
use std::io::{self, Read, Write};
use std::io::{BufRead, BufReader};
pub use record::Record;
pub use record::RecordKind;
use serde::Serialize;
use serde_json::Serializer;
pub use summary::Summary;
/// A collection of logged statistics for the various Harper frontends.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Stats {
pub records: Vec<Record>,
}
@ -32,23 +36,31 @@ impl Stats {
summary
}
pub fn write_csv(&self, w: &mut impl Write) -> io::Result<()> {
let mut writer = csv::WriterBuilder::new().has_headers(false).from_writer(w);
/// Write the records from `self`.
/// Expects the target buffer to either be empty or already be terminated by a newline.
pub fn write(&self, w: &mut impl Write) -> io::Result<()> {
for record in &self.records {
writer.serialize(record)?;
let mut serializer = Serializer::new(&mut *w);
record.serialize(&mut serializer)?;
writeln!(w)?;
}
Ok(())
}
pub fn read_csv(r: &mut impl Read) -> io::Result<Self> {
let mut reader = csv::ReaderBuilder::new().has_headers(false).from_reader(r);
/// Read records from a buffer into `self`.
/// Assumes the buffer is properly formatted and terminated with a newline.
/// An empty buffer will result in no mutation to `self`.
pub fn read(r: &mut impl Read) -> io::Result<Self> {
let br = BufReader::new(r);
let mut records = Vec::new();
for result in reader.deserialize() {
let record: Record = result?;
for line_res in br.lines() {
let line = line_res?;
dbg!(&line);
let record: Record = serde_json::from_str(&line)?;
records.push(record);
}
@ -61,3 +73,38 @@ impl Default for Stats {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use quickcheck::Arbitrary;
use quickcheck_macros::quickcheck;
use crate::{Record, Stats};
impl Arbitrary for Stats {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut stats = Stats::new();
for _ in 0..g.size() {
stats.records.push(Record::arbitrary(g));
}
stats
}
}
#[quickcheck]
fn io_is_reversible(ex: Stats) -> bool {
let mut written = Vec::new();
ex.write(&mut written).unwrap();
let mut readable = Cursor::new(written);
let read = Stats::read(&mut readable).unwrap();
ex == read
}
}

View file

@ -4,7 +4,7 @@ use harper_core::linting::LintKind;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
pub struct Record {
pub kind: RecordKind,
/// Recorded as seconds from the Unix Epoch
@ -23,7 +23,42 @@ impl Record {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Eq, PartialEq)]
pub enum RecordKind {
Lint(LintKind),
}
#[cfg(test)]
mod tests {
use harper_core::linting::LintKind;
use quickcheck::Arbitrary;
use super::{Record, RecordKind};
impl Arbitrary for RecordKind {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
*g.choose(&[
Self::Lint(LintKind::Spelling),
Self::Lint(LintKind::Capitalization),
Self::Lint(LintKind::Style),
Self::Lint(LintKind::Formatting),
Self::Lint(LintKind::Repetition),
Self::Lint(LintKind::Enhancement),
Self::Lint(LintKind::Readability),
Self::Lint(LintKind::WordChoice),
Self::Lint(LintKind::Miscellaneous),
])
.unwrap()
}
}
impl Arbitrary for Record {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Record {
kind: RecordKind::arbitrary(g),
when: u64::arbitrary(g),
uuid: uuid::Builder::from_u128(u128::arbitrary(g)).into_uuid(),
}
}
}
}