The Scala Toolkit: Your First Essential Library
Introduction
The Scala Toolkit is a curated collection of libraries that makes common programming tasks easier and more enjoyable. It's designed to be the "batteries included" experience for Scala, providing high-quality, well-tested libraries for everyday tasks like file I/O, JSON processing, HTTP requests, and more.
In this lesson, you'll learn about the Scala Toolkit, how to set it up in your projects, and explore its most useful components. This will be your first step into Scala's rich ecosystem of libraries and tools.
What is the Scala Toolkit?
The Scala Toolkit is an initiative by the Scala organization to provide:
- Curated Libraries: Hand-picked, high-quality libraries for common tasks
- Consistent API: Libraries that work well together
- Easy Setup: Simple dependency management
- Good Documentation: Clear examples and guides
- Community Endorsed: Libraries recommended by the Scala community
Core Components
The toolkit includes several essential libraries:
- os-lib: For file system operations and running processes
- upickle: For JSON serialization and deserialization
- sttp: For HTTP client operations
- ujson: For JSON parsing and manipulation
- munit: For testing (covered in later lessons)
Setting Up the Scala Toolkit
Adding to Your Project
In your build.sbt
file:
// For Scala 3
libraryDependencies += "org.scala-lang" %% "toolkit" % "0.2.1"
// For Scala 2.13
libraryDependencies += "org.scala-lang" %% "toolkit" % "0.2.1"
// For testing support (optional)
libraryDependencies += "org.scala-lang" %% "toolkit-test" % "0.2.1" % Test
Simple sbt Project Setup
Here's a complete minimal build.sbt
:
ThisBuild / scalaVersion := "3.3.1"
ThisBuild / version := "0.1.0"
lazy val root = (project in file("."))
.settings(
name := "scala-toolkit-example",
libraryDependencies += "org.scala-lang" %% "toolkit" % "0.2.1"
)
Working with Files: os-lib
The os-lib
library makes file operations simple and safe.
Basic File Operations
import os.*
// Reading files
val content = os.read(os.pwd / "example.txt")
println(content)
// Writing files
os.write(os.pwd / "output.txt", "Hello, Scala Toolkit!")
// Check if file exists
if (os.exists(os.pwd / "config.json")) {
println("Config file found!")
}
// List directory contents
val files = os.list(os.pwd)
files.foreach(println)
// Create directories
os.makeDir.all(os.pwd / "data" / "processed")
Path Manipulation
import os.*
// Working with paths
val projectRoot = os.pwd
val dataDir = projectRoot / "data"
val inputFile = dataDir / "input.csv"
val outputFile = dataDir / "processed" / "output.json"
println(s"Project root: $projectRoot")
println(s"Input file: $inputFile")
println(s"File name: ${inputFile.last}")
println(s"File extension: ${inputFile.ext}")
// Relative paths
val relativeToData = inputFile.relativeTo(dataDir)
println(s"Relative path: $relativeToData")
File Information
import os.*
import java.time.Instant
val file = os.pwd / "example.txt"
if (os.exists(file)) {
// File properties
val size = os.size(file)
val modified = Instant.ofEpochMilli(os.mtime(file))
val isDir = os.isDir(file)
val isFile = os.isFile(file)
println(s"Size: $size bytes")
println(s"Modified: $modified")
println(s"Is directory: $isDir")
println(s"Is file: $isFile")
// Permissions (Unix-like systems)
if (!os.isWindows) {
val perms = os.perms(file)
println(s"Permissions: $perms")
}
}
Advanced File Operations
import os.*
// Copy files and directories
os.copy(os.pwd / "source.txt", os.pwd / "backup" / "source.txt")
// Move/rename files
os.move(os.pwd / "old_name.txt", os.pwd / "new_name.txt")
// Remove files and directories
os.remove(os.pwd / "temp.txt")
os.remove.all(os.pwd / "temp_directory") // Remove directory and contents
// Find files
val scalaFiles = os.walk(os.pwd)
.filter(_.ext == "scala")
.filter(os.isFile)
scalaFiles.foreach(file => println(s"Found Scala file: $file"))
// Find with pattern
val logFiles = os.walk(os.pwd / "logs")
.filter(_.last.startsWith("app-"))
.filter(_.ext == "log")
Working with JSON: upickle and ujson
JSON Parsing with ujson
import ujson.*
// Parse JSON string
val jsonString = """{"name": "Alice", "age": 30, "active": true}"""
val json = ujson.read(jsonString)
// Access values
val name = json("name").str
val age = json("age").num.toInt
val active = json("active").bool
println(s"Name: $name, Age: $age, Active: $active")
// Working with arrays
val arrayJson = """[1, 2, 3, 4, 5]"""
val numbers = ujson.read(arrayJson).arr.map(_.num.toInt)
println(s"Numbers: ${numbers.mkString(", ")}")
// Nested objects
val complexJson = """{
"user": {
"name": "Bob",
"preferences": {
"theme": "dark",
"notifications": true
}
}
}"""
val parsed = ujson.read(complexJson)
val theme = parsed("user")("preferences")("theme").str
println(s"Theme: $theme")
JSON Serialization with upickle
import upickle.default.*
// Simple case class
case class Person(name: String, age: Int, active: Boolean)
// Serialize to JSON
val person = Person("Alice", 30, true)
val jsonString = write(person)
println(jsonString) // {"name":"Alice","age":30,"active":true}
// Deserialize from JSON
val personFromJson = read[Person](jsonString)
println(personFromJson) // Person(Alice,30,true)
// Working with collections
val people = List(
Person("Alice", 30, true),
Person("Bob", 25, false),
Person("Charlie", 35, true)
)
val peopleJson = write(people)
println(peopleJson)
val peopleFromJson = read[List[Person]](peopleJson)
peopleFromJson.foreach(println)
Advanced JSON Operations
import ujson.*
import upickle.default.*
// Custom serialization
case class User(id: Int, name: String, email: Option[String] = None)
implicit val userRW: ReadWriter[User] = macroRW
// Working with Option fields
val users = List(
User(1, "Alice", Some("alice@example.com")),
User(2, "Bob", None),
User(3, "Charlie", Some("charlie@test.com"))
)
val usersJson = write(users, indent = 2)
println(usersJson)
// Manual JSON construction
val manualJson = ujson.Obj(
"status" -> "success",
"data" -> ujson.Obj(
"users" -> users.length,
"active" -> users.count(_.email.isDefined)
),
"timestamp" -> System.currentTimeMillis()
)
println(manualJson.render(indent = 2))
HTTP Requests: sttp
Basic HTTP Requests
import sttp.client3.*
// Create a backend
val backend = HttpURLConnectionBackend()
// Simple GET request
val request = basicRequest.get(uri"https://api.github.com/users/scala")
val response = request.send(backend)
println(s"Status: ${response.code}")
println(s"Body: ${response.body}")
// Clean up
backend.close()
Working with JSON APIs
import sttp.client3.*
import ujson.*
case class GitHubUser(login: String, name: Option[String], public_repos: Int)
implicit val githubUserRW: ReadWriter[GitHubUser] = macroRW
def fetchGitHubUser(username: String): Either[String, GitHubUser] = {
val backend = HttpURLConnectionBackend()
try {
val request = basicRequest
.get(uri"https://api.github.com/users/$username")
.header("User-Agent", "Scala-Toolkit-Example")
val response = request.send(backend)
response.code.code match {
case 200 =>
Right(read[GitHubUser](response.body.getOrElse("")))
case 404 =>
Left(s"User $username not found")
case code =>
Left(s"API error: $code")
}
} catch {
case ex: Exception => Left(s"Request failed: ${ex.getMessage}")
} finally {
backend.close()
}
}
// Usage
fetchGitHubUser("scala") match {
case Right(user) =>
println(s"User: ${user.login}, Repos: ${user.public_repos}")
case Left(error) =>
println(s"Error: $error")
}
POST Requests and Form Data
import sttp.client3.*
import ujson.*
val backend = HttpURLConnectionBackend()
// JSON POST request
val userData = ujson.Obj(
"name" -> "Alice",
"email" -> "alice@example.com"
)
val jsonRequest = basicRequest
.post(uri"https://httpbin.org/post")
.header("Content-Type", "application/json")
.body(userData.render())
val jsonResponse = jsonRequest.send(backend)
println(s"JSON Response: ${jsonResponse.body}")
// Form data POST
val formRequest = basicRequest
.post(uri"https://httpbin.org/post")
.body(Map("name" -> "Bob", "email" -> "bob@example.com"))
val formResponse = formRequest.send(backend)
println(s"Form Response: ${formResponse.body}")
backend.close()
Practical Examples
Example 1: Configuration Manager
import os.*
import upickle.default.*
import ujson.*
case class DatabaseConfig(
host: String,
port: Int,
database: String,
username: String,
password: String
)
case class AppConfig(
appName: String,
version: String,
debug: Boolean,
database: DatabaseConfig
)
implicit val dbConfigRW: ReadWriter[DatabaseConfig] = macroRW
implicit val appConfigRW: ReadWriter[AppConfig] = macroRW
object ConfigManager {
private val configFile = os.pwd / "config.json"
def loadConfig(): Option[AppConfig] = {
if (os.exists(configFile)) {
try {
val content = os.read(configFile)
Some(read[AppConfig](content))
} catch {
case ex: Exception =>
println(s"Error reading config: ${ex.getMessage}")
None
}
} else {
None
}
}
def saveConfig(config: AppConfig): Boolean = {
try {
val jsonContent = write(config, indent = 2)
os.write(configFile, jsonContent)
true
} catch {
case ex: Exception =>
println(s"Error saving config: ${ex.getMessage}")
false
}
}
def createDefaultConfig(): AppConfig = {
AppConfig(
appName = "MyScalaApp",
version = "1.0.0",
debug = true,
database = DatabaseConfig(
host = "localhost",
port = 5432,
database = "myapp",
username = "user",
password = "password"
)
)
}
}
// Usage
val config = ConfigManager.loadConfig().getOrElse {
println("No config found, creating default...")
val defaultConfig = ConfigManager.createDefaultConfig()
ConfigManager.saveConfig(defaultConfig)
defaultConfig
}
println(s"App: ${config.appName} v${config.version}")
println(s"Database: ${config.database.host}:${config.database.port}")
Example 2: Web Scraper and Data Processor
import os.*
import sttp.client3.*
import ujson.*
import upickle.default.*
case class Article(title: String, url: String, timestamp: Long)
implicit val articleRW: ReadWriter[Article] = macroRW
object NewsScraper {
private val backend = HttpURLConnectionBackend()
private val dataDir = os.pwd / "data"
def fetchHackerNewsTop(): List[Article] = {
try {
// Get top story IDs
val topStoriesRequest = basicRequest
.get(uri"https://hacker-news.firebaseio.com/v0/topstories.json")
val topStoriesResponse = topStoriesRequest.send(backend)
val storyIds = read[List[Int]](topStoriesResponse.body.getOrElse("[]"))
// Fetch first 10 stories
storyIds.take(10).flatMap { id =>
try {
val storyRequest = basicRequest
.get(uri"https://hacker-news.firebaseio.com/v0/item/$id.json")
val storyResponse = storyRequest.send(backend)
val storyJson = ujson.read(storyResponse.body.getOrElse("{}"))
Some(Article(
title = storyJson("title").str,
url = storyJson.obj.get("url").map(_.str).getOrElse(s"https://news.ycombinator.com/item?id=$id"),
timestamp = System.currentTimeMillis()
))
} catch {
case ex: Exception =>
println(s"Error fetching story $id: ${ex.getMessage}")
None
}
}
} catch {
case ex: Exception =>
println(s"Error fetching top stories: ${ex.getMessage}")
List.empty
}
}
def saveArticles(articles: List[Article]): Unit = {
os.makeDir.all(dataDir)
val filename = s"articles-${System.currentTimeMillis()}.json"
val filepath = dataDir / filename
val jsonContent = write(articles, indent = 2)
os.write(filepath, jsonContent)
println(s"Saved ${articles.length} articles to $filepath")
}
def loadAllArticles(): List[Article] = {
if (!os.exists(dataDir)) return List.empty
os.list(dataDir)
.filter(_.ext == "json")
.filter(_.last.startsWith("articles-"))
.flatMap { file =>
try {
val content = os.read(file)
read[List[Article]](content)
} catch {
case ex: Exception =>
println(s"Error reading $file: ${ex.getMessage}")
List.empty
}
}
.toList
}
def cleanup(): Unit = {
backend.close()
}
}
// Usage
val articles = NewsScraper.fetchHackerNewsTop()
println(s"Fetched ${articles.length} articles")
articles.foreach { article =>
println(s"- ${article.title}")
println(s" ${article.url}")
}
NewsScraper.saveArticles(articles)
// Load and display all saved articles
val allArticles = NewsScraper.loadAllArticles()
println(s"\nTotal saved articles: ${allArticles.length}")
NewsScraper.cleanup()
Example 3: Log File Analyzer
import os.*
import ujson.*
import upickle.default.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
case class LogEntry(
timestamp: LocalDateTime,
level: String,
message: String,
thread: Option[String] = None
)
case class LogAnalysis(
totalEntries: Int,
levelCounts: Map[String, Int],
errorMessages: List[String],
timeRange: (LocalDateTime, LocalDateTime),
mostActiveHour: Int
)
implicit val localDateTimeRW: ReadWriter[LocalDateTime] = readwriter[String].bimap(
dt => dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
str => LocalDateTime.parse(str, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
)
implicit val logEntryRW: ReadWriter[LogEntry] = macroRW
implicit val logAnalysisRW: ReadWriter[LogAnalysis] = macroRW
object LogAnalyzer {
private val timestampPattern = """(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})""".r
private val logPattern = """(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\](?: \[([^\]]+)\])? (.+)""".r
def parseLogFile(logFile: os.Path): List[LogEntry] = {
if (!os.exists(logFile)) {
println(s"Log file not found: $logFile")
return List.empty
}
val lines = os.read.lines(logFile)
lines.flatMap { line =>
line match {
case logPattern(timestamp, level, thread, message) =>
try {
val dt = LocalDateTime.parse(timestamp, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
Some(LogEntry(dt, level, message, Option(thread)))
} catch {
case ex: Exception =>
println(s"Error parsing timestamp: $timestamp")
None
}
case _ => None
}
}.toList
}
def analyzeEntries(entries: List[LogEntry]): LogAnalysis = {
val levelCounts = entries.groupBy(_.level).view.mapValues(_.size).toMap
val errorMessages = entries.filter(_.level == "ERROR").map(_.message)
val timestamps = entries.map(_.timestamp)
val timeRange = (timestamps.min, timestamps.max)
val hourCounts = entries.groupBy(_.timestamp.getHour).view.mapValues(_.size)
val mostActiveHour = hourCounts.maxBy(_._2)._1
LogAnalysis(
totalEntries = entries.length,
levelCounts = levelCounts,
errorMessages = errorMessages,
timeRange = timeRange,
mostActiveHour = mostActiveHour
)
}
def generateReport(analysis: LogAnalysis): String = {
val (startTime, endTime) = analysis.timeRange
s"""
|Log Analysis Report
|==================
|
|Total Entries: ${analysis.totalEntries}
|Time Range: $startTime to $endTime
|Most Active Hour: ${analysis.mostActiveHour}:00
|
|Level Distribution:
|${analysis.levelCounts.toSeq.sortBy(-_._2).map { case (level, count) =>
s" $level: $count"
}.mkString("\n")}
|
|Error Messages:
|${if (analysis.errorMessages.nonEmpty)
analysis.errorMessages.zipWithIndex.map { case (msg, idx) =>
s" ${idx + 1}. $msg"
}.mkString("\n")
else " No errors found"
}
|""".stripMargin
}
def saveAnalysis(analysis: LogAnalysis, outputFile: os.Path): Unit = {
val jsonContent = write(analysis, indent = 2)
os.write(outputFile, jsonContent)
println(s"Analysis saved to $outputFile")
}
}
// Create sample log file for testing
val sampleLogContent = """2023-08-31 10:15:30 [INFO] Application started
2023-08-31 10:15:35 [DEBUG] Loading configuration from config.json
2023-08-31 10:15:40 [INFO] Database connection established
2023-08-31 10:15:45 [WARN] Deprecated API method used
2023-08-31 10:16:00 [ERROR] Failed to process user request: timeout
2023-08-31 10:16:15 [INFO] Processing 100 records
2023-08-31 10:16:30 [ERROR] Database connection lost
2023-08-31 10:16:45 [INFO] Reconnected to database
2023-08-31 10:17:00 [DEBUG] Cache hit rate: 85%"""
val logFile = os.pwd / "sample.log"
os.write(logFile, sampleLogContent)
// Analyze the log file
val entries = LogAnalyzer.parseLogFile(logFile)
val analysis = LogAnalyzer.analyzeEntries(entries)
val report = LogAnalyzer.generateReport(analysis)
println(report)
// Save analysis
LogAnalyzer.saveAnalysis(analysis, os.pwd / "log-analysis.json")
// Clean up
os.remove(logFile)
Best Practices
1. Resource Management
import sttp.client3.*
// Always close backends
def makeHttpRequest(url: String): String = {
val backend = HttpURLConnectionBackend()
try {
val request = basicRequest.get(uri"$url")
val response = request.send(backend)
response.body.getOrElse("")
} finally {
backend.close()
}
}
// Or use resource management
def withBackend[T](f: SttpBackend[Identity, Any] => T): T = {
val backend = HttpURLConnectionBackend()
try {
f(backend)
} finally {
backend.close()
}
}
2. Error Handling
import scala.util.{Try, Success, Failure}
def safeFileOperation(file: os.Path): Either[String, String] = {
Try {
os.read(file)
} match {
case Success(content) => Right(content)
case Failure(exception) => Left(s"Failed to read file: ${exception.getMessage}")
}
}
3. Configuration Validation
case class Config(host: String, port: Int, timeout: Int) {
require(host.nonEmpty, "Host cannot be empty")
require(port > 0 && port < 65536, "Port must be between 1 and 65535")
require(timeout > 0, "Timeout must be positive")
}
def loadValidatedConfig(): Either[String, Config] = {
Try {
val configFile = os.pwd / "config.json"
val content = os.read(configFile)
read[Config](content)
} match {
case Success(config) => Right(config)
case Failure(exception) => Left(s"Config error: ${exception.getMessage}")
}
}
Summary
In this lesson, you've learned about the Scala Toolkit and its core components:
✅ Scala Toolkit: Curated collection of essential libraries
✅ os-lib: Safe and intuitive file system operations
✅ upickle/ujson: JSON serialization and parsing
✅ sttp: HTTP client for API interactions
✅ Integration: How libraries work together seamlessly
✅ Real-world Examples: Configuration management, web scraping, log analysis
✅ Best Practices: Resource management and error handling
The Scala Toolkit provides a solid foundation for building real applications. These libraries handle common tasks elegantly while maintaining type safety and functional programming principles.
What's Next
In the next lesson, we'll start Part 2: "Object-Oriented Programming in Scala" with "Classes and Objects: The Blueprint of Your Application." You'll learn how to define classes, create instances, and understand the role of constructors in Scala.
This marks an important transition from basic language features to building structured, object-oriented applications that can model real-world domains.
Ready to start building with objects and classes? Let's continue!
Comments
Be the first to comment on this lesson!