Skip to content

Commit 3c70268

Browse files
reid-spencerclaude
andcommitted
Add scope-based parsing API for 10 RIDDL definition types
Enables parsing individual RIDDL files at specific scope levels (Domain, Context, Entity, Epic, Streamlet, Module, Adaptor, Projector, Repository, Saga) needed by Synapify visual editor. - Add 10 parseAs* methods to TopLevelParser companion object - Expose through RiddlLib (cross-platform) and RiddlAPI (JS facade) - Add TypeScript type definitions with branded opaque types - Remove include support from Function definitions - Add 38 new test cases (17 parser-level + 21 RiddlLib-level) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c574b73 commit 3c70268

File tree

12 files changed

+1565
-13
lines changed

12 files changed

+1565
-13
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright 2019-2026 Ossum Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ossuminc.riddl.language.parsing
8+
9+
import com.ossuminc.riddl.utils.{pc, ec}
10+
11+
class ScopedParsingTestJVM extends ScopedParsingTest

language/shared/src/main/scala/com/ossuminc/riddl/language/AST.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -778,8 +778,10 @@ object AST:
778778
/** Type of definitions that occur in a [[Function]] */
779779
private type OccursInFunction = OccursInVitalDefinition | Statement | Function
780780

781-
/** Type of definitions that occur in a [[Function]], with Include */
782-
type FunctionContents = OccursInFunction | Include[OccursInFunction]
781+
/** Type of definitions that occur in a [[Function]].
782+
* Functions are self-contained and do not support includes.
783+
*/
784+
type FunctionContents = OccursInFunction
783785

784786
/** Type of definitions that occur in a [[Type]] */
785787
type TypeContents = Field | Method | Enumerator

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/EpicParser.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ private[parsing] trait EpicParser {
151151
include[u, EpicContents]((p: P[?]) => epicDefinitions(using p.asInstanceOf[P[u]]))
152152
}
153153

154-
private def epicDefinitions[u: P]: P[Seq[EpicContents]] = {
154+
private[parsing] def epicDefinitions[u: P]: P[Seq[EpicContents]] = {
155155
P(vitalDefinitionContents | useCase | shownBy | epicInclude).asInstanceOf[P[EpicContents]].rep(1)
156156
}
157157

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/ExtensibleTopLevelParser.scala

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,67 @@ trait ExtensibleTopLevelParser(using PlatformContext)
174174
}
175175
}
176176

177+
/** Parse the input as the contents of a Domain definition.
178+
* Wraps the input in a synthetic `domain _scope_ is { ... }`
179+
* and returns the resulting Domain.
180+
*/
181+
def parseDomainContents: Either[Messages, Domain] =
182+
doParse[Domain](p => domain(using p))
183+
184+
/** Parse the input as the contents of a Context definition.
185+
* Wraps the input in a synthetic `context _scope_ is { ... }`
186+
* and returns the resulting Context.
187+
*/
188+
def parseContextContents: Either[Messages, Context] =
189+
doParse[Context](p => context(using p))
190+
191+
/** Parse the input as the contents of an Entity definition.
192+
* Wraps the input in a synthetic `entity _scope_ is { ... }`
193+
* and returns the resulting Entity.
194+
*/
195+
def parseEntityContents: Either[Messages, Entity] =
196+
doParse[Entity](p => entity(using p))
197+
198+
/** Parse the input as the contents of a Module definition. */
199+
def parseModuleContents: Either[Messages, Module] =
200+
doParse[Module](p => module(using p))
201+
202+
/** Parse the input as the contents of an Adaptor definition. */
203+
def parseAdaptorContents: Either[Messages, Adaptor] =
204+
doParse[Adaptor](p => adaptor(using p))
205+
206+
/** Parse the input as the contents of a Projector definition. */
207+
def parseProjectorContents: Either[Messages, Projector] =
208+
doParse[Projector](p => projector(using p))
209+
210+
/** Parse the input as the contents of a Repository definition. */
211+
def parseRepositoryContents: Either[Messages, Repository] =
212+
doParse[Repository](p => repository(using p))
213+
214+
/** Parse the input as epic body content (use cases, types,
215+
* etc.) and return a raw sequence of EpicContents.
216+
*/
217+
def parseEpicDefinitions: Either[Messages, Seq[EpicContents]] =
218+
doContentsParse[EpicContents](p => epicDefinitions(using p))
219+
.map(_.toSeq)
220+
221+
/** Parse the input as saga body content (saga steps,
222+
* inlets, outlets, functions) and return a raw sequence.
223+
*/
224+
def parseSagaDefinitions: Either[Messages, Seq[SagaContents]] =
225+
doContentsParse[SagaContents](p => sagaDefinitions(using p))
226+
.map(_.toSeq)
227+
228+
/** Parse the input as streamlet processor content (handlers,
229+
* types, functions, etc.) and return a raw sequence.
230+
*/
231+
def parseStreamletDefinitions: Either[Messages, Seq[StreamletContents]] =
232+
doContentsParse[StreamletContents] { (p: P[?]) =>
233+
given P[Any] = p.asInstanceOf[P[Any]]
234+
processorDefinitionContents(StatementsSet.StreamStatements)
235+
.asInstanceOf[P[StreamletContents]].rep(1)
236+
}.map(_.toSeq)
237+
177238
def parseTokens: Either[Messages, List[Token]] = {
178239
parse[List[Token]](input, p => parseAllTokens(using p)) match
179240
case Left((messages, _)) => Left(messages)

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/FunctionParser.scala

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ import fastparse.MultiLineWhitespace.*
1515
private[parsing] trait FunctionParser {
1616
this: VitalDefinitionParser & StatementParser =>
1717

18-
private def functionInclude[u: P]: P[Include[FunctionContents]] = {
19-
include[u, FunctionContents]((p: P[?]) => functionDefinitions(using p.asInstanceOf[P[u]]))
20-
}
21-
2218
def funcInput[u: P]: P[Aggregation] = {
2319
P(Keywords.requires ~ aggregation)./
2420
}
@@ -30,7 +26,7 @@ private[parsing] trait FunctionParser {
3026
private def functionDefinitions[u: P]: P[Seq[FunctionContents]] = {
3127
P(
3228
undefined(Seq.empty[FunctionContents]) | (
33-
vitalDefinitionContents | function | functionInclude | statement(
29+
vitalDefinitionContents | function | statement(
3430
StatementsSet.FunctionStatements
3531
)
3632
).asInstanceOf[P[FunctionContents]]./.rep(0)
@@ -56,7 +52,6 @@ private[parsing] trait FunctionParser {
5652
P(
5753
Index ~ Keywords.function ~/ identifier ~ is ~ open ~/ functionBody ~ close ~ withMetaData ~/ Index
5854
)./.map { case (start, id, (ins, outs, contents), descriptives, end) =>
59-
checkForDuplicateIncludes(contents)
6055
Function(at(start, end), id, ins, outs, contents.toContents, descriptives.toContents)
6156
}
6257
}

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/SagaParser.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private[parsing] trait SagaParser {
2929
include[u, SagaContents]((p: P[?]) => sagaDefinitions(using p.asInstanceOf[P[u]]))
3030
}
3131

32-
private def sagaDefinitions[u: P]: P[Seq[SagaContents]] = {
32+
private[parsing] def sagaDefinitions[u: P]: P[Seq[SagaContents]] = {
3333
P(
3434
sagaStep | inlet | outlet | function | sagaInclude
3535
).asInstanceOf[P[SagaContents]]./.rep(2)

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/TopLevelParser.scala

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,235 @@ object TopLevelParser:
246246
Right(mapped)
247247
end match
248248
end mapTextAndToken
249+
250+
/** Parse content as if it were inside a Domain body.
251+
*
252+
* Wraps the input in a synthetic `domain SyntheticScope is { ... }`
253+
* declaration, parses it, and returns the resulting Domain.
254+
*
255+
* @param input The RiddlParserInput containing domain-level content
256+
* @param withVerboseFailures Enable verbose parse failure messages
257+
* @return Either error messages or the parsed Domain
258+
*/
259+
def parseAsDomain(
260+
input: RiddlParserInput,
261+
withVerboseFailures: Boolean = false
262+
)(using PlatformContext): Either[Messages, Domain] =
263+
val wrapped = RiddlParserInput(
264+
s"domain SyntheticScope is {\n${input.data}\n}",
265+
input.root
266+
)
267+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
268+
tlp.parseDomainContents
269+
end parseAsDomain
270+
271+
/** Parse content as if it were inside a Context body.
272+
*
273+
* Wraps the input in a synthetic `context SyntheticScope is { ... }`
274+
* declaration, parses it, and returns the resulting Context.
275+
*
276+
* @param input The RiddlParserInput containing context-level content
277+
* @param withVerboseFailures Enable verbose parse failure messages
278+
* @return Either error messages or the parsed Context
279+
*/
280+
def parseAsContext(
281+
input: RiddlParserInput,
282+
withVerboseFailures: Boolean = false
283+
)(using PlatformContext): Either[Messages, Context] =
284+
val wrapped = RiddlParserInput(
285+
s"context SyntheticScope is {\n${input.data}\n}",
286+
input.root
287+
)
288+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
289+
tlp.parseContextContents
290+
end parseAsContext
291+
292+
/** Parse content as if it were inside an Entity body.
293+
*
294+
* Wraps the input in a synthetic `entity SyntheticScope is { ... }`
295+
* declaration, parses it, and returns the resulting Entity.
296+
*
297+
* @param input The RiddlParserInput containing entity-level content
298+
* @param withVerboseFailures Enable verbose parse failure messages
299+
* @return Either error messages or the parsed Entity
300+
*/
301+
def parseAsEntity(
302+
input: RiddlParserInput,
303+
withVerboseFailures: Boolean = false
304+
)(using PlatformContext): Either[Messages, Entity] =
305+
val wrapped = RiddlParserInput(
306+
s"entity SyntheticScope is {\n${input.data}\n}",
307+
input.root
308+
)
309+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
310+
tlp.parseEntityContents
311+
end parseAsEntity
312+
313+
/** Parse content as if it were inside a Module body.
314+
*
315+
* Wraps the input in a synthetic `module SyntheticScope is { ... }`
316+
* declaration, parses it, and returns the resulting Module.
317+
*/
318+
def parseAsModule(
319+
input: RiddlParserInput,
320+
withVerboseFailures: Boolean = false
321+
)(using PlatformContext): Either[Messages, Module] =
322+
val wrapped = RiddlParserInput(
323+
s"module SyntheticScope is {\n${input.data}\n}",
324+
input.root
325+
)
326+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
327+
tlp.parseModuleContents
328+
end parseAsModule
329+
330+
/** Parse content as if it were inside an Adaptor body.
331+
*
332+
* Wraps the input in a synthetic adaptor declaration using
333+
* the caller-provided direction and context reference, then
334+
* parses it and returns the resulting Adaptor.
335+
*
336+
* @param input The RiddlParserInput containing adaptor body content
337+
* @param direction The AdaptorDirection (InboundAdaptor or OutboundAdaptor)
338+
* @param contextRef The ContextRef the adaptor adapts from/to
339+
* @param withVerboseFailures Enable verbose parse failure messages
340+
* @return Either error messages or the parsed Adaptor
341+
*/
342+
def parseAsAdaptor(
343+
input: RiddlParserInput,
344+
direction: AdaptorDirection,
345+
contextRef: ContextRef,
346+
withVerboseFailures: Boolean = false
347+
)(using PlatformContext): Either[Messages, Adaptor] =
348+
val dirStr = direction match
349+
case _: InboundAdaptor => "from"
350+
case _: OutboundAdaptor => "to"
351+
val ctxPath = contextRef.pathId.value.mkString(".")
352+
val wrapped = RiddlParserInput(
353+
s"adaptor SyntheticScope $dirStr context $ctxPath is {\n${input.data}\n}",
354+
input.root
355+
)
356+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
357+
tlp.parseAdaptorContents
358+
end parseAsAdaptor
359+
360+
/** Parse content as if it were inside a Projector body.
361+
*
362+
* Wraps the input in a synthetic `projector SyntheticScope is { ... }`
363+
* declaration, parses it, and returns the resulting Projector.
364+
*/
365+
def parseAsProjector(
366+
input: RiddlParserInput,
367+
withVerboseFailures: Boolean = false
368+
)(using PlatformContext): Either[Messages, Projector] =
369+
val wrapped = RiddlParserInput(
370+
s"projector SyntheticScope is {\n${input.data}\n}",
371+
input.root
372+
)
373+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
374+
tlp.parseProjectorContents
375+
end parseAsProjector
376+
377+
/** Parse content as if it were inside a Repository body.
378+
*
379+
* Wraps the input in a synthetic `repository SyntheticScope is { ... }`
380+
* declaration, parses it, and returns the resulting Repository.
381+
*/
382+
def parseAsRepository(
383+
input: RiddlParserInput,
384+
withVerboseFailures: Boolean = false
385+
)(using PlatformContext): Either[Messages, Repository] =
386+
val wrapped = RiddlParserInput(
387+
s"repository SyntheticScope is {\n${input.data}\n}",
388+
input.root
389+
)
390+
val tlp = new TopLevelParser(wrapped, withVerboseFailures)
391+
tlp.parseRepositoryContents
392+
end parseAsRepository
393+
394+
/** Parse content as if it were inside a Saga body.
395+
*
396+
* Parses the input for saga body content (saga steps,
397+
* functions, inlets, outlets) and assembles a Saga using
398+
* the caller-provided input/output aggregations.
399+
*
400+
* @param input The RiddlParserInput containing saga body content
401+
* @param sagaInput Optional input aggregation from the parent Saga
402+
* @param sagaOutput Optional output aggregation from the parent Saga
403+
* @param withVerboseFailures Enable verbose parse failure messages
404+
* @return Either error messages or the parsed Saga
405+
*/
406+
def parseAsSaga(
407+
input: RiddlParserInput,
408+
sagaInput: Option[Aggregation] = None,
409+
sagaOutput: Option[Aggregation] = None,
410+
withVerboseFailures: Boolean = false
411+
)(using PlatformContext): Either[Messages, Saga] =
412+
val tlp = new TopLevelParser(input, withVerboseFailures)
413+
tlp.parseSagaDefinitions.map { contents =>
414+
val loc =
415+
if contents.nonEmpty then contents.head.loc
416+
else At.empty
417+
Saga(loc, Identifier.empty, sagaInput, sagaOutput,
418+
contents.toContents)
419+
}
420+
end parseAsSaga
421+
422+
/** Parse content as if it were inside an Epic body.
423+
*
424+
* Parses the input for epic body content (use cases, types,
425+
* etc.) and assembles an Epic using the caller-provided
426+
* UserStory.
427+
*
428+
* @param input The RiddlParserInput containing epic-level content
429+
* @param userStory The UserStory from the parent Epic definition
430+
* @param withVerboseFailures Enable verbose parse failure messages
431+
* @return Either error messages or the parsed Epic
432+
*/
433+
def parseAsEpic(
434+
input: RiddlParserInput,
435+
userStory: UserStory,
436+
withVerboseFailures: Boolean = false
437+
)(using PlatformContext): Either[Messages, Epic] =
438+
val tlp = new TopLevelParser(input, withVerboseFailures)
439+
tlp.parseEpicDefinitions.map { contents =>
440+
val loc =
441+
if contents.nonEmpty then contents.head.loc
442+
else At.empty
443+
Epic(loc, Identifier.empty, userStory, contents.toContents)
444+
}
445+
end parseAsEpic
446+
447+
/** Parse content as if it were inside a Streamlet body.
448+
*
449+
* Parses the input for processor content (handlers, types,
450+
* functions, etc.) and assembles a Streamlet using the
451+
* caller-provided shape, inlets, and outlets.
452+
*
453+
* @param input The RiddlParserInput containing streamlet body content
454+
* @param shape The StreamletShape from the parent Streamlet definition
455+
* @param inlets The Inlet definitions from the parent Streamlet
456+
* @param outlets The Outlet definitions from the parent Streamlet
457+
* @param withVerboseFailures Enable verbose parse failure messages
458+
* @return Either error messages or the parsed Streamlet
459+
*/
460+
def parseAsStreamlet(
461+
input: RiddlParserInput,
462+
shape: StreamletShape,
463+
inlets: Seq[Inlet],
464+
outlets: Seq[Outlet],
465+
withVerboseFailures: Boolean = false
466+
)(using PlatformContext): Either[Messages, Streamlet] =
467+
val tlp = new TopLevelParser(input, withVerboseFailures)
468+
tlp.parseStreamletDefinitions.map { contents =>
469+
val allContents =
470+
(inlets.asInstanceOf[Seq[StreamletContents]] ++
471+
outlets.asInstanceOf[Seq[StreamletContents]] ++
472+
contents)
473+
val loc =
474+
if allContents.nonEmpty then allContents.head.loc
475+
else At.empty
476+
Streamlet(loc, Identifier.empty, shape, allContents.toContents)
477+
}
478+
end parseAsStreamlet
479+
249480
end TopLevelParser

0 commit comments

Comments
 (0)