**ComponentRoutes.scala:**
```scala
package component
import zio.*
import zio.http.*
import scala.reflect.Selectable.reflectiveSelectable
/** Routes for component HTMX handlers */
object ComponentRoutes:
/** Component handler route: POST /summer/component/{alias}/{handler} */
val routes: Routes[ComponentManager & TemplateService & CsrfService, Response] =
Routes(
// HTMX handler endpoint
Method.POST / "summer" / "component" / string("alias") / string("handler") ->
handler { (alias: String, handlerName: String, req: Request) =>
handleComponentRequest(alias, handlerName, req)
},
// Component render endpoint (for initial page load)
Method.GET / "summer" / "component" / string("alias") / "render" ->
handler { (alias: String, req: Request) =>
renderComponent(alias, req)
}
)
private def handleComponentRequest(
alias: String,
handlerName: String,
req: Request
): ZIO[ComponentManager & TemplateService & CsrfService, Nothing, Response] =
(for
// Validate CSRF token
_ <- CsrfService.validateRequest(req)
// Validate handler name starts with "on"
_ <- ZIO.fail(ComponentError.InvalidHandler(alias, handlerName, "Handler must start with 'on'"))
.when(!handlerName.startsWith("on"))
// Create page context from request
ctx = pageContextFromRequest(req)
// Extract properties from request body
props <- extractPropertiesFromRequest(req)
// Create component instance
manager <- ZIO.service[ComponentManager]
component <- manager.create(alias, props, ctx)
// Initialize component
_ <- component.init.provideEnvironment(ZEnvironment(ctx))
// Invoke the handler using reflection
result <- invokeHandler(component, handlerName, req, ctx)
yield result.toResponse).catchAll { error =>
// Return error as HTML fragment with error styling
ZIO.succeed(errorResponse(error))
}
private def renderComponent(
alias: String,
req: Request
): ZIO[ComponentManager & TemplateService, Nothing, Response] =
(for
ctx = pageContextFromRequest(req)
props <- extractPropertiesFromRequest(req)
manager <- ZIO.service[ComponentManager]
component <- manager.create(alias, props, ctx)
_ <- component.init.provideEnvironment(ZEnvironment(ctx))
_ <- component.onRun.provideEnvironment(ZEnvironment(ctx))
// Render default template
templateService <- ZIO.service[TemplateService]
templatePath = component.templateDir.resolve("default.htm")
propsMap <- component.properties.get
html <- templateService.render(templatePath, buildTemplateContext(component, propsMap))
yield Response.html(html.content)).catchAll { error =>
ZIO.succeed(errorResponse(error))
}
private def pageContextFromRequest(req: Request): PageContext =
PageContext(
url = req.url.path.toString,
params = req.url.queryParams.map.view.mapValues(_.headOption.getOrElse("")).toMap,
isBackendEditor = req.header("X-Summer-Editor").isDefined
)
private def extractPropertiesFromRequest(req: Request): UIO[Map[String, PropertyValue]] =
// For now, return empty map - full form parsing in later enhancement
// Properties typically come from page/layout configuration, not request
ZIO.succeed(Map.empty)
/**
* Invoke a handler method on the component.
* Handler methods must:
* - Start with "on" (e.g., onRefresh, onLoadMore)
* - Return ZIO[ComponentEnv, ComponentError, HtmxResponse]
*
* Note: Uses Scala 3 reflection. For production, consider compile-time macro.
*/
private def invokeHandler(
component: SummerComponent,
handlerName: String,
req: Request,
ctx: PageContext
): ZIO[TemplateService, ComponentError, HtmxResponse] =
ZIO.attemptBlocking {
// Use reflection to find and invoke the handler method
val cls = component.getClass
val method = cls.getMethod(handlerName)
method.invoke(component)
}.flatMap {
case zio: ZIO[?, ?, ?] =>
// The handler returns a ZIO - we need to run it
zio.asInstanceOf[ZIO[ComponentEnv, ComponentError, HtmxResponse]]
.provideEnvironment(ZEnvironment(ctx))
case response: HtmxResponse =>
// Handler returned a direct response
ZIO.succeed(response)
case other =>
ZIO.fail(ComponentError.InvalidHandler(
component.alias,
handlerName,
s"Handler must return ZIO[ComponentEnv, ComponentError, HtmxResponse], got ${other.getClass}"
))
}.catchAll {
case ce: ComponentError => ZIO.fail(ce)
case e: NoSuchMethodException =>
ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, "Handler method not found"))
case e: Throwable =>
ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, e.getMessage))
}
private def buildTemplateContext(
component: SummerComponent,
properties: Map[String, PropertyValue]
): Map[String, Any] =
Map(
"__SELF__" -> component.alias,
"_component" -> component.details
) ++ properties.view.mapValues(pvToAny).toMap
private def pvToAny(pv: PropertyValue): Any = pv match
case PropertyValue.StringVal(s) => s
case PropertyValue.BoolVal(b) => b
case PropertyValue.IntVal(i) => i
case PropertyValue.ListVal(l) => l.map(pvToAny)
case PropertyValue.ObjectVal(m) => m.view.mapValues(pvToAny).toMap
case PropertyValue.NullVal => null
private def errorResponse(error: ComponentError): Response =
val html = s"""
Component Error: ${error.message}
"""
Response.html(html).status(Status.InternalServerError)
```
**package.scala:**
```scala
package object component:
import zio.*
/** Combined layer for all component services */
type ComponentServices = ComponentManager & TemplateService & CsrfService
/** Create all component service layers */
val componentLayer: ZLayer[Any, Nothing, ComponentServices] =
ComponentManager.live ++ TemplateService.live ++ CsrfService.live
```
**Update api/Routes.scala** to include component routes:
Read existing Routes.scala and add component routes:
```scala
import zio.http.*
import component.{ComponentRoutes, componentLayer}
object Routes:
/** Combined routes for the application */
val routes: Routes[Any, Response] =
HealthRoutes.routes ++ LandingRoutes.routes ++
ComponentRoutes.routes.provideLayer(componentLayer)
```
Note: The exact integration depends on existing Routes.scala structure. The key is to combine ComponentRoutes.routes with existing routes and provide the componentLayer.
**Update Main.scala** to provide component services:
Read existing Main.scala and update the layer composition. The key changes:
1. Import component package: `import component.componentLayer`
2. Add componentLayer to the provide chain for routes
The updated provide for startupLogic should include:
```scala
// In the routes Server.serve line:
res <- Server.serve(Routes.routes).provide(
Server.defaultWithPort(cfg.server.port),
QuillContext.dataSourceLayer,
componentLayer
)
```
However, if routes are already using the layer internally (as shown above), Main.scala may not need changes.
Full Main.scala update:
```scala
import zio.*
import zio.http.*
import zio.config.typesafe.TypesafeConfigProvider
import api.Routes
import _root_.config.{AppConfig as SummerConfig}
import db.QuillContext
import plugin.{PluginManager, PluginDiscovery, pluginLayer}
import component.{ComponentManager, componentLayer}
object Main extends ZIOAppDefault {
private val banner: String =
"""
|
| | .
| `. * | .'
| `. ._|_* .' .
| . * .' `. *
| -------| |-------
| . *`.___.' * .
| .' |* `. *
| .' * | . `.
| . |
|
| S U M M E R C M S
|""".stripMargin
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.setConfigProvider(TypesafeConfigProvider.fromResourcePath())
private val startupLogic: ZIO[PluginManager & PluginDiscovery & ComponentManager, Any, Nothing] =
for {
cfg <- ZIO.config[SummerConfig](SummerConfig.config)
_ <- Console.printLine(banner)
_ <- Console.printLine(s"Starting on port ${cfg.server.port}...")
_ <- Console.printLine("")
// Initialize plugin system
_ <- Console.printLine("Loading plugins...")
_ <- PluginManager.loadPlugins(PluginDiscovery.defaultPluginsDir)
.catchAll(e => Console.printLine(s"Plugin loading warning: ${e.message}").as(()))
_ <- PluginManager.bootAll
.catchAll(e => Console.printLine(s"Plugin boot warning: ${e.message}").as(()))
plugins <- PluginManager.listPlugins
_ <- Console.printLine(s"Loaded ${plugins.count(_._2.isActive)} plugin(s)")
// Initialize component system
components <- ComponentManager.listComponents
_ <- Console.printLine(s"Registered ${components.size} component(s)")
_ <- Console.printLine("")
// Note: Migrations are NOT auto-run. Use CLI to run migrations (Phase 5).
res <- Server.serve(Routes.routes).provide(
Server.defaultWithPort(cfg.server.port),
QuillContext.dataSourceLayer
)
} yield res
override def run: ZIO[Any, Any, Any] =
startupLogic.provide(pluginLayer ++ componentLayer)
}
```