-
Notifications
You must be signed in to change notification settings - Fork 119
Expand file tree
/
Copy pathLinkChecker.scala
More file actions
67 lines (60 loc) · 2.79 KB
/
Copy pathLinkChecker.scala
File metadata and controls
67 lines (60 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package demo
import kyo.*
/** Link checker - client-only demo (no server).
*
* Fetches a web page, extracts all href links, then checks each link in parallel. Reports status for each link. Demonstrates pure
* HttpClient usage and concurrent fan-out with Async.foreach.
*/
object LinkChecker extends KyoApp:
case class LinkResult(url: String, status: Int, ok: Boolean) derives Schema
/** Extract href values from HTML using a simple regex. */
def extractLinks(html: String, baseUrl: String): Seq[String] =
val pattern = """href=["']([^"']+)["']""".r
pattern.findAllMatchIn(html).map(_.group(1)).toSeq.distinct
.map { link =>
if link.startsWith("http://") || link.startsWith("https://") then link
else if link.startsWith("//") then "https:" + link
else if link.startsWith("/") then
val uri = new java.net.URI(baseUrl)
s"${uri.getScheme}://${uri.getHost}$link"
else if link.startsWith("#") || link.startsWith("mailto:") || link.startsWith("javascript:") then ""
else baseUrl.stripSuffix("/") + "/" + link
}
.filter(_.startsWith("http"))
.distinct
end extractLinks
/** Check a single link by sending a HEAD request. */
def checkLink(url: String): LinkResult < (Async & Abort[HttpException]) =
HttpClient.withConfig(_.timeout(10.seconds).followRedirects(true)) {
HttpClient.getText(url).map { _ =>
LinkResult(url, 200, true)
}
}
run {
val targetUrl = "https://www.scala-lang.org"
for
_ <- Console.printLine(s"Fetching $targetUrl...")
html <- HttpClient.withConfig(_.timeout(15.seconds)) {
HttpClient.getText(targetUrl)
}
links = extractLinks(html, targetUrl)
_ <- Console.printLine(s"Found ${links.size} links. Checking...")
results <- Async.foreach(links, links.size) { url =>
Abort.run[HttpException](checkLink(url)).map {
case kyo.Result.Success(r) => r
case kyo.Result.Failure(_) => LinkResult(url, 0, false)
case kyo.Result.Panic(_) => LinkResult(url, 0, false)
}
}
sorted = results.sortBy(r => (!r.ok, r.url))
_ <- Kyo.foreach(sorted) { r =>
val symbol = if r.ok then "OK" else "FAIL"
Console.printLine(s" [$symbol] ${r.status} ${r.url}")
}
okCount = results.count(_.ok)
failCount = results.size - okCount
_ <- Console.printLine(s"\nDone: $okCount ok, $failCount failed out of ${results.size} links")
yield ()
end for
}
end LinkChecker