Skip to content

Commit a72623f

Browse files
authored
Add typeshare for automatic server-side type conversion (#664)
1 parent 4c73a45 commit a72623f

24 files changed

+125
-42
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ publish = false
77
[workspace]
88
members = ["crates/*", "tools/changelog-generator", "tools/scraper"]
99

10-
[dependencies]
10+
[workspace.dependencies]
1111
anyhow = "1.0.98"
12+
chrono = "0.4.41"
13+
serde = { version = "1.0.219", features = ["derive"] }
14+
serde_json = "1.0.140"
15+
tokio = { version = "1.45.0", features = ["rt-multi-thread", "macros"] }
16+
typeshare = "1.0.4"
17+
18+
[dependencies]
19+
anyhow = { workspace = true }
1220
async-mongodb-session = { version = "3.0.0", default-features = false }
1321
async-session = "3.0.0"
1422
axum = { version = "0.7.9", features = ["json"] }
1523
axum-extra = { version = "0.9.6", features = ["cookie", "typed-header"] }
1624
axum-server = "0.6.0"
1725
base64 = "0.21.7"
1826
bytes = "1.10.1"
19-
chrono = "0.4.41"
27+
chrono = { workspace = true }
2028
clap = { version = "4.5.38", features = ["derive"] }
2129
db = { path = "crates/db" }
2230
dotenv = "0.15.0"
@@ -30,14 +38,15 @@ rand = "0.9.1"
3038
reqwest = { version = "0.11.27", default-features = false, features = [ "blocking", "json", "rustls-tls"] }
3139
rusoto_core = { version = "0.48.0", default-features = false, features = [ "rustls", ] }
3240
rusoto_s3 = { version = "0.48.0", default-features = false, features = [ "rustls", ] }
33-
serde = { version = "1.0.219", features = ["derive"] }
34-
serde_json = "1.0.140"
41+
serde = { workspace = true }
42+
serde_json = { workspace = true }
3543
sha2 = "0.10.9"
36-
tokio = { version = "1.45.0", features = ["rt-multi-thread", "macros"] }
44+
tokio = { workspace = true }
3745
tower = { version = "0.4.13", features = ["tracing", "limit", "buffer"] }
3846
tower-http = { version = "0.5.2", features = ["cors", "fs", "trace"] }
3947
tower_governor = "0.2.0"
4048
tracing = "0.1.41"
49+
typeshare = { workspace = true }
4150
url = "2.5.4"
4251
walkdir = "2.5.0"
4352

client/src/components/CourseAverages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io';
66
import { Link } from 'react-router-dom';
77
import { twMerge } from 'tailwind-merge';
88

9+
import { Instructor } from '../lib/types';
910
import { compareTerms } from '../lib/utils';
1011
import { Course } from '../model/Course';
11-
import { Instructor } from '../model/Instructor';
1212
import { TermAverage } from '../model/TermAverage';
1313

1414
type InstructorLinkProps = {

client/src/lib/repo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Subscription } from '../lib/types';
2+
import type { UserResponse } from '../lib/types';
13
import { GetCourseReviewsInteractionPayload } from '../model/GetCourseReviewsInteractionsPayload';
24
import type { GetCourseWithReviewsPayload } from '../model/GetCourseWithReviewsPayload';
35
import { GetCoursesPayload } from '../model/GetCoursesPayload';
@@ -7,8 +9,6 @@ import { GetReviewsPayload } from '../model/GetReviewsPayload';
79
import type { InteractionKind } from '../model/Interaction';
810
import type { Notification } from '../model/Notification';
911
import type { SearchResults } from '../model/SearchResults';
10-
import type { Subscription } from '../model/Subscription';
11-
import type { UserResponse } from '../model/User';
1212

1313
const prefix = '/api';
1414

client/src/lib/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Generated by typeshare 1.11.0
3+
*/
4+
5+
export interface Instructor {
6+
name: string;
7+
nameNgrams?: string;
8+
term: string;
9+
}
10+
11+
export interface Subscription {
12+
courseId: string;
13+
userId: string;
14+
}
15+
16+
export interface User {
17+
id: string;
18+
mail: string;
19+
}
20+
21+
export interface UserResponse {
22+
user?: User;
23+
}

client/src/lib/utils.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { groupBy } from 'lodash';
22

3+
import { Instructor } from '../lib/types';
34
import { Course } from '../model/Course';
4-
import { Instructor } from '../model/Instructor';
55
import type { Schedule } from '../model/Schedule';
66

77
const TERM_ORDER = ['Winter', 'Summer', 'Fall'];
88

99
/**
1010
* Groups course instructors by their terms, but only for current terms.
11+
*
1112
* Creates empty arrays for current terms that have no instructors.
13+
*
1214
* @param {Course} course - The course object containing instructors and terms
1315
* @returns {Record<string, Instructor[]>} Object mapping terms to arrays of instructors
1416
*/
@@ -33,9 +35,11 @@ export const groupCurrentCourseTermInstructors = (
3335

3436
/**
3537
* Determines the current and next two academic terms based on the current date.
38+
*
3639
* - May-July: Returns [Summer current, Fall current, Winter next]
3740
* - August-December: Returns [Fall current, Winter next, Summer next]
3841
* - January-April: Returns [Fall previous, Winter current, Summer current]
42+
*
3943
* @returns {[string, string, string]} Array of three consecutive terms
4044
*/
4145
export const getCurrentTerms = (): [string, string, string] => {
@@ -57,9 +61,11 @@ export const getCurrentTerms = (): [string, string, string] => {
5761

5862
/**
5963
* Determines the current academic term based on the current date.
64+
*
6065
* - May-July: Summer <year>
6166
* - August-December: Fall <year>
6267
* - January-April: Winter <year>
68+
*
6369
* @returns string The current term
6470
*/
6571
export const getCurrentTerm = (): string => {
@@ -80,7 +86,9 @@ export const getCurrentTerm = (): string => {
8086

8187
/**
8288
* Compares two academic terms for sorting.
89+
*
8390
* Terms are compared first by year, then by season according to TERM_ORDER.
91+
*
8492
* @param {string} a - First term string (e.g., "Fall 2023")
8593
* @param {string} b - Second term string (e.g., "Winter 2024")
8694
* @returns {number} Negative if a comes before b, positive if b comes before a, 0 if equal
@@ -93,7 +101,9 @@ export const compareTerms = (a: string, b: string): number => {
93101

94102
/**
95103
* Sorts an array of academic terms chronologically.
104+
*
96105
* Uses compareTerms to determine order.
106+
*
97107
* @param {string[]} terms - Array of term strings to sort
98108
* @returns {string[]} Sorted array of terms
99109
*/
@@ -103,8 +113,11 @@ export const sortTerms = (terms: string[]): string[] => {
103113

104114
/**
105115
* Sorts course schedules by block type and number.
116+
*
106117
* Order priority: Lec > Lab > Seminar > Tut > Conf
118+
*
107119
* Within each type, sorts numerically by block number.
120+
*
108121
* @param {Schedule[]} schedules - Array of course schedules to sort
109122
* @returns {Schedule[]} Sorted array of schedules
110123
*/
@@ -125,7 +138,9 @@ export const sortSchedulesByBlocks = (schedules: Schedule[]): Schedule[] => {
125138

126139
/**
127140
* Converts a course ID to a URL-friendly parameter format.
141+
*
128142
* Example: "COMP202" -> "comp-202"
143+
*
129144
* @param {string} courseId - The course ID to convert
130145
* @returns {string} URL-friendly course ID
131146
*/
@@ -134,6 +149,7 @@ export const courseIdToUrlParam = (courseId: string): string =>
134149

135150
/**
136151
* Capitalizes the first character of a string.
152+
*
137153
* @param {string} s - The string to capitalize
138154
* @returns {string} Capitalized string
139155
*/
@@ -142,7 +158,9 @@ export const capitalize = (s: string): string =>
142158

143159
/**
144160
* Ensures a string ends with a period.
161+
*
145162
* Adds a period if one is not already present.
163+
*
146164
* @param {string} s - The string to punctuate
147165
* @returns {string} String ending with a period
148166
*/
@@ -153,8 +171,11 @@ const COURSE_CODE_REGEX = /^(([A-Z0-9]){4} [0-9]{3}(D1|D2|N1|N2|J1|J2|J3)?)$/;
153171

154172
/**
155173
* Validates if a string matches the course code format.
174+
*
156175
* Valid format: 4 alphanumeric chars + space + 3 digits + optional suffix
176+
*
157177
* Suffixes: D1, D2, N1, N2, J1, J2, J3
178+
*
158179
* @param {string} s - The string to validate
159180
* @returns {boolean} True if string is a valid course code
160181
*/
@@ -163,7 +184,9 @@ export const isValidCourseCode = (s: string): boolean =>
163184

164185
/**
165186
* Inserts a delimiter between the subject and number portions of a course code.
187+
*
166188
* Example: spliceCourseCode("COMP202", "-") -> "COMP-202"
189+
*
167190
* @param {string} courseCode - The course code to splice
168191
* @param {string} delimiter - The delimiter to insert
169192
* @returns {string} Course code with delimiter inserted
@@ -175,14 +198,17 @@ export const spliceCourseCode = (
175198

176199
/**
177200
* Rounds a number to 2 decimal places.
201+
*
178202
* @param {number} n - Number to round
179203
* @returns {number} Rounded number
180204
*/
181205
export const round2Decimals = (n: number): number => Math.round(n * 100) / 100;
182206

183207
/**
184208
* Performs a true modulo operation (different from JavaScript's % operator).
209+
*
185210
* Always returns a positive number, even when inputs are negative.
211+
*
186212
* @param {number} n - Dividend
187213
* @param {number} m - Divisor
188214
* @returns {number} Positive modulo result
@@ -191,6 +217,7 @@ export const mod = (n: number, m: number): number => ((n % m) + m) % m;
191217

192218
/**
193219
* Custom error class for date-related errors.
220+
*
194221
* @extends Error
195222
*/
196223
export class InvalidDateError extends Error {
@@ -202,8 +229,11 @@ export class InvalidDateError extends Error {
202229

203230
/**
204231
* Converts a date to a human-readable "time ago" string.
232+
*
205233
* Handles various time units from seconds to years.
234+
*
206235
* Throws InvalidDateError for null, undefined, invalid formats, or future dates.
236+
*
207237
* @param {Date | string | number | null | undefined} date - Date to convert
208238
* @returns {string} Human-readable time difference (e.g., "2 hours ago")
209239
* @throws {InvalidDateError} If date is invalid, missing, or in the future

client/src/model/Course.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Instructor } from './Instructor';
1+
import type { Instructor } from '../lib/types';
22
import type { ReqNode } from './Requirements';
33
import type { Schedule } from './Schedule';
44

client/src/model/GetInstructorPayload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Instructor } from './Instructor';
1+
import type { Instructor } from '../lib/types';
22
import type { Review } from './Review';
33

44
export type GetInstructorPayload = {

client/src/model/Instructor.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

client/src/model/Subscription.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)