Skip to content

Commit b2d3a6b

Browse files
authored
feat: add /latest endpoint (#1223)
Implements a new API endpoint that allows fetching the latest version of a snippet without knowing its update number. Users can now access snippets via /{login}/{uuid}/latest to always get the most recent version.
1 parent ddd6848 commit b2d3a6b

File tree

14 files changed

+186
-18
lines changed

14 files changed

+186
-18
lines changed

balancer/src/main/scala/org/scastie/balancer/DispatchActor.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ case class DownloadSnippet(snippetId: SnippetId)
4141
case class ForkSnippet(snippetId: SnippetId, inputs: InputsWithIpAndUser)
4242

4343
case class FetchSnippet(snippetId: SnippetId)
44+
case class FetchLatestSnippet(snippetId: SnippetId)
4445
case class FetchOldSnippet(id: Int)
4546
case class FetchUserSnippets(user: User)
4647

@@ -193,6 +194,10 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef)
193194
val sender = this.sender()
194195
logError(container.readSnippet(snippetId).map(sender ! _))
195196

197+
case FetchLatestSnippet(snippetId) =>
198+
val sender = this.sender()
199+
logError(container.readLatestSnippet(snippetId).map(sender ! _))
200+
196201
case FetchOldSnippet(id) =>
197202
val sender = this.sender()
198203
logError(container.readOldSnippet(id).map(sender ! _))

client/src/main/scala/org/scastie/client/RestApiClient.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ class RestApiClient(serverUrl: Option[String]) extends RestApi {
9898
def fetch(snippetId: SnippetId): Future[Option[FetchResult]] =
9999
get[FetchResult]("/snippets/" + snippetId.url)
100100

101+
def fetchLatest(snippetId: SnippetId): Future[Option[FetchResult]] =
102+
snippetId.user match {
103+
case Some(SnippetUserPart(login, _)) =>
104+
get[FetchResult](s"/snippets/$login/${snippetId.base64UUID}/latest")
105+
case None =>
106+
fetch(snippetId)
107+
}
108+
101109
def fetchOld(id: Int): Future[Option[FetchResult]] =
102110
get[FetchResult](s"/old-snippets/$id")
103111

client/src/main/scala/org/scastie/client/Routing.scala

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class Routing(defaultServerUrl: String) {
8585
val anon = snippetId
8686
val user = alpha / snippetId
8787
val userUpdate = alpha / snippetId / int
88+
val userLatest = alpha / snippetId / "latest"
8889
val oldId = int
8990

9091
(
@@ -101,6 +102,8 @@ class Routing(defaultServerUrl: String) {
101102
dynRenderR((page, router) => renderOldSnippetIdPage(page, router))
102103
| dynamicRouteCT(anon.caseClass[AnonymousResource]) ~>
103104
dynRenderR((page, router) => renderPage(page, router))
105+
| dynamicRouteCT(userLatest.caseClass[UserResourceLatest]) ~>
106+
dynRenderR((page, router) => renderPage(page, router))
104107
| dynamicRouteCT(user.caseClass[UserResource]) ~>
105108
dynRenderR((page, router) => renderPage(page, router))
106109
| dynamicRouteCT(userUpdate.caseClass[UserResourceUpdated]) ~>
@@ -151,33 +154,32 @@ class Routing(defaultServerUrl: String) {
151154
private def renderPage(page: ResourcePage, router: RouterCtl[Page]): VdomElement = {
152155
val defaultEmbedded = Some(EmbeddedOptions.empty(defaultServerUrl))
153156

154-
val (embedded, snippetId) = page match {
157+
case class PageInfo(embedded: Option[EmbeddedOptions], snippetId: SnippetId)
158+
159+
val pageInfo = page match {
155160
case AnonymousResource(uuid) =>
156-
val snippetId = SnippetId(uuid, None)
157-
(None, snippetId)
161+
PageInfo(None, SnippetId(uuid, None))
158162
case UserResource(login, uuid) =>
159-
val snippetId = SnippetId(uuid, Some(SnippetUserPart(login)))
160-
(None, snippetId)
163+
PageInfo(None, SnippetId(uuid, Some(SnippetUserPart(login))))
164+
case UserResourceLatest(login, uuid) =>
165+
/* Latest is represented with update -1 */
166+
PageInfo(None, SnippetId(uuid, Some(SnippetUserPart(login, -1))))
161167
case UserResourceUpdated(login, uuid, update) =>
162-
val snippetId = SnippetId(uuid, Some(SnippetUserPart(login, update)))
163-
(None, snippetId)
168+
PageInfo(None, SnippetId(uuid, Some(SnippetUserPart(login, update))))
164169
case EmbeddedAnonymousResource(uuid) =>
165-
val snippetId = SnippetId(uuid, None)
166-
(defaultEmbedded, snippetId)
170+
PageInfo(defaultEmbedded, SnippetId(uuid, None))
167171
case EmbeddedUserResource(login, uuid) =>
168-
val snippetId = SnippetId(uuid, Some(SnippetUserPart(login)))
169-
(defaultEmbedded, snippetId)
172+
PageInfo(defaultEmbedded, SnippetId(uuid, Some(SnippetUserPart(login))))
170173
case EmbeddedUserResourceUpdated(login, uuid, update) =>
171-
val snippetId = SnippetId(uuid, Some(SnippetUserPart(login, update)))
172-
(defaultEmbedded, snippetId)
174+
PageInfo(defaultEmbedded, SnippetId(uuid, Some(SnippetUserPart(login, update))))
173175
}
174176

175177
Scastie(
176178
scastieId = UUID.randomUUID(),
177179
router = Some(router),
178-
snippetId = Some(snippetId),
180+
snippetId = Some(pageInfo.snippetId),
179181
oldSnippetId = None,
180-
embedded = embedded,
182+
embedded = pageInfo.embedded,
181183
targetType = None,
182184
tryLibrary = None,
183185
code = None,

client/src/main/scala/org/scastie/client/RoutingADT.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ case class UserResourceUpdated(
4343
update: Int
4444
) extends ResourcePage
4545

46+
case class UserResourceLatest(
47+
login: String,
48+
uuid: String
49+
) extends ResourcePage
50+
4651
case class EmbeddedAnonymousResource(
4752
uuid: String
4853
) extends ResourcePage

client/src/main/scala/org/scastie/client/ScastieBackend.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,14 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac
423423
)
424424
}
425425

426+
def loadLatestSnippet(snippetId: SnippetId): Callback = {
427+
loadSnippetBase(
428+
restApiClient.fetchLatest(snippetId),
429+
afterLoading = _.setSnippetId(snippetId),
430+
snippetId = Some(snippetId)
431+
)
432+
}
433+
426434
private def loadSnippetBase(
427435
fetchSnippet: => Future[Option[FetchResult]],
428436
afterLoading: ScastieState => ScastieState = identity,

client/src/main/scala/org/scastie/client/components/Scastie.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ object Scastie {
118118
val initialState = props.embedded match {
119119
case None => {
120120
props.snippetId match {
121-
case Some(snippetId) => backend.loadSnippet(snippetId)
121+
case Some(snippetId) =>
122+
snippetId.user match {
123+
case Some(SnippetUserPart(_, -1)) => backend.loadLatestSnippet(snippetId)
124+
case _ => backend.loadSnippet(snippetId)
125+
}
122126

123127
case None => props.oldSnippetId match {
124128
case Some(id) => backend.loadOldSnippet(id)
@@ -136,7 +140,11 @@ object Scastie {
136140
}
137141
case Some(embededOptions) => {
138142
val setInputs = (embededOptions.snippetId, embededOptions.inputs) match {
139-
case (Some(snippetId), _) => backend.loadSnippet(snippetId)
143+
case (Some(snippetId), _) =>
144+
snippetId.user match {
145+
case Some(SnippetUserPart(_, -1)) => backend.loadLatestSnippet(snippetId)
146+
case _ => backend.loadSnippet(snippetId)
147+
}
140148

141149
case (_, Some(inputs)) => backend.scope.modState(_.setInputs(inputs))
142150

server/src/main/scala/org/scastie/server/RestApiServer.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class RestApiServer(
8181
.mapTo[Option[FetchResult]]
8282
}
8383

84+
def fetchLatest(snippetId: SnippetId): Future[Option[FetchResult]] = {
85+
dispatchActor
86+
.ask(FetchLatestSnippet(snippetId))
87+
.mapTo[Option[FetchResult]]
88+
}
89+
8490
def fetchOld(id: Int): Future[Option[FetchResult]] = {
8591
dispatchActor
8692
.ask(FetchOldSnippet(id))

server/src/main/scala/org/scastie/server/routes/ApiRoutes.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class ApiRoutes(
6262
encodeResponseWith(Gzip)(
6363
get(
6464
concat(
65+
snippetIdLatest("snippets")(
66+
sid => complete(server.fetchLatest(sid))
67+
),
6568
snippetIdStart("snippets")(
6669
sid => complete(server.fetch(sid))
6770
),

server/src/main/scala/org/scastie/server/routes/package.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ package object routes {
2424
matcherStart / _ / matcherEnd
2525
)(f)
2626

27+
def snippetIdLatest(matcherStart: String)(f: SnippetId => Route): Route =
28+
path(matcherStart / Segment / uuidMatcher / "latest" ~ Slash.?) { (user, uuid) =>
29+
f(SnippetId(uuid, Some(SnippetUserPart(user, -1))))
30+
}
31+
2732
def snippetIdExtension(extension: String)(f: SnippetId => Route): Route =
2833
snippetIdBase(
2934
_ ~ extension,

storage/src/main/scala/org/scastie/storage/SnippetsContainer.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ trait SnippetsContainer {
4949
snippetId: SnippetId
5050
): Future[Option[FetchResultScalaJsSourceMap]]
5151
def readSnippet(snippetId: SnippetId): Future[Option[FetchResult]]
52+
def readLatestSnippet(snippetId: SnippetId): Future[Option[FetchResult]]
5253
protected def insert(snippetId: SnippetId, inputs: BaseInputs): Future[Unit]
5354
protected def hideFromUserProfile(snippetId: SnippetId): Future[Unit]
5455

0 commit comments

Comments
 (0)