1use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use serde_json::{Map, Number, Value};
6use syn::{
7 Attribute, Error, FnArg, Ident, ImplItem, ItemImpl, Lit, LitStr, Result, Token, Type, braced,
8 bracketed,
9 ext::IdentExt,
10 parse::{Parse, ParseStream},
11 parse_macro_input,
12 punctuated::Punctuated,
13 spanned::Spanned,
14};
15
16#[proc_macro_attribute]
24pub fn endpoint(arguments: TokenStream, input: TokenStream) -> TokenStream {
25 let register_plugin = if arguments.is_empty() {
26 true
27 } else {
28 let mode = parse_macro_input!(arguments as Ident);
29 if mode != "standalone" {
30 return Error::new_spanned(
31 mode,
32 "endpoint accepts only the internal `standalone` mode",
33 )
34 .into_compile_error()
35 .into();
36 }
37 false
38 };
39
40 let implementation = parse_macro_input!(input as ItemImpl);
41 expand_endpoint_with_registration(implementation, register_plugin)
42 .unwrap_or_else(Error::into_compile_error)
43 .into()
44}
45
46#[proc_macro]
51pub fn openapi_operation(input: TokenStream) -> TokenStream {
52 let operation = parse_macro_input!(input as OpenApiObject);
53 match operation_literal(&operation) {
54 Ok(operation) => quote!(#operation).into(),
55 Err(error) => error.into_compile_error().into(),
56 }
57}
58
59#[cfg(test)]
60fn expand_endpoint(implementation: ItemImpl) -> Result<proc_macro2::TokenStream> {
61 expand_endpoint_with_registration(implementation, true)
62}
63
64fn expand_endpoint_with_registration(
65 mut implementation: ItemImpl,
66 register_plugin: bool,
67) -> Result<proc_macro2::TokenStream> {
68 if implementation.trait_.is_some() {
69 return Err(Error::new_spanned(
70 implementation.impl_token,
71 "endpoint can only be applied to an inherent impl block",
72 ));
73 }
74 if !implementation.generics.params.is_empty() {
75 return Err(Error::new_spanned(
76 &implementation.generics,
77 "endpoint does not support generic impl blocks",
78 ));
79 }
80
81 let provider = implementation.self_ty.clone();
82 let provider_middlewares = take_provider_middlewares(&mut implementation.attrs)?;
83 let mut routes = Vec::new();
84 for item in &mut implementation.items {
85 let ImplItem::Fn(method) = item else {
86 continue;
87 };
88 let metadata = take_handler_metadata(&mut method.attrs)?;
89 if let Some(mut route) = metadata.route {
90 route.openapi = metadata.openapi;
91 routes.push(Handler {
92 route,
93 middlewares: provider_middlewares
94 .iter()
95 .cloned()
96 .chain(metadata.middlewares)
97 .collect(),
98 method: method.sig.ident.clone(),
99 arguments: handler_arguments(method)?,
100 });
101 } else if !metadata.middlewares.is_empty() || metadata.openapi.is_some() {
102 return Err(Error::new_spanned(
103 &method.sig.ident,
104 "endpoint metadata can only be attached to an HTTP handler",
105 ));
106 }
107 }
108 if routes.is_empty() {
109 return Err(Error::new_spanned(
110 &implementation.self_ty,
111 "endpoint impl must declare at least one HTTP handler attribute",
112 ));
113 }
114
115 let const_routes = routes.iter().map(endpoint_route);
116 let implementation_routes = routes.iter().map(endpoint_route);
117 let dispatch_arms = routes.iter().map(dispatch_arm);
118 let plugin_registration =
119 register_plugin.then(|| quote!(#[lenso::provides(http_endpoint_contract::Endpoint)]));
120
121 Ok(quote! {
122 #implementation
123
124 const _: () = {
125 const ROUTES: &[::lenso_capability_http_endpoint::EndpointRoute] = &[
126 #(#const_routes)*
127 ];
128 ::lenso_capability_http_endpoint::__private::validate_endpoint_routes(ROUTES);
129 };
130
131 #plugin_registration
132 impl ::lenso_capability_http_endpoint::HttpEndpoint for #provider {
133 const ROUTES: &'static [::lenso_capability_http_endpoint::EndpointRoute] = &[
134 #(#implementation_routes)*
135 ];
136
137 fn dispatch(
138 &self,
139 context: ::lenso_capability_http_endpoint::__private::InvocationContext,
140 request: ::lenso_capability_http_endpoint::HandleRequest,
141 ) -> ::lenso_capability_http_endpoint::EndpointFuture {
142 let provider = self.clone();
143 Box::pin(async move {
144 let route_id = request.route_id.clone();
145 match route_id.as_str() {
146 #(#dispatch_arms,)*
147 _ => Ok(Err(::lenso_capability_http_endpoint::HandleError::Rejected)),
148 }
149 })
150 }
151 }
152 })
153}
154
155fn endpoint_route(handler: &Handler) -> proc_macro2::TokenStream {
156 let route_id = &handler.route.id;
157 let method = &handler.route.method;
158 let path = &handler.route.path;
159 let openapi = handler
160 .route
161 .openapi
162 .as_ref()
163 .map(|operation| quote!(.with_openapi(#operation)));
164 quote! {
165 ::lenso_capability_http_endpoint::EndpointRoute::new(
166 #route_id,
167 #method,
168 #path,
169 ) #openapi,
170 }
171}
172
173fn take_provider_middlewares(attributes: &mut Vec<Attribute>) -> Result<Vec<Ident>> {
174 let mut middlewares = Vec::new();
175 let mut retained = Vec::with_capacity(attributes.len());
176 for attribute in attributes.drain(..) {
177 if attribute.path().is_ident("middleware") {
178 let arguments =
179 attribute.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated)?;
180 if arguments.is_empty() {
181 return Err(Error::new_spanned(
182 attribute,
183 "middleware requires at least one provider method",
184 ));
185 }
186 middlewares.extend(arguments);
187 } else {
188 retained.push(attribute);
189 }
190 }
191 *attributes = retained;
192 Ok(middlewares)
193}
194
195fn take_handler_metadata(attributes: &mut Vec<Attribute>) -> Result<HandlerMetadata> {
196 let mut route = None;
197 let mut middlewares = Vec::new();
198 let mut openapi = None;
199 let mut retained = Vec::with_capacity(attributes.len());
200 for attribute in attributes.drain(..) {
201 if attribute.path().is_ident("middleware") {
202 let arguments =
203 attribute.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated)?;
204 if arguments.is_empty() {
205 return Err(Error::new_spanned(
206 attribute,
207 "middleware requires at least one provider method",
208 ));
209 }
210 middlewares.extend(arguments);
211 continue;
212 }
213 if attribute.path().is_ident("openapi") {
214 if openapi.is_some() {
215 return Err(Error::new_spanned(
216 attribute,
217 "an endpoint handler may declare only one OpenAPI Operation Object",
218 ));
219 }
220 let operation = attribute.parse_args::<OpenApiOperation>()?.into_literal()?;
221 openapi = Some(operation);
222 continue;
223 }
224 let Some(http_method) = http_method(&attribute) else {
225 retained.push(attribute);
226 continue;
227 };
228 if route.is_some() {
229 return Err(Error::new_spanned(
230 attribute,
231 "an endpoint handler may declare only one HTTP method",
232 ));
233 }
234 let arguments = attribute.parse_args::<RouteArguments>()?;
235 route = Some(Route {
236 method: LitStr::new(http_method, attribute.path().span()),
237 id: arguments.route_id,
238 path: arguments.path,
239 openapi: None,
240 });
241 }
242 *attributes = retained;
243 Ok(HandlerMetadata {
244 route,
245 middlewares,
246 openapi,
247 })
248}
249
250enum OpenApiOperation {
251 Json(LitStr),
252 Object(OpenApiObject),
253}
254
255impl OpenApiOperation {
256 fn into_literal(self) -> Result<LitStr> {
257 match self {
258 Self::Json(operation) => {
259 validate_openapi_operation(&operation)?;
260 Ok(operation)
261 }
262 Self::Object(operation) => operation_literal(&operation),
263 }
264 }
265}
266
267impl Parse for OpenApiOperation {
268 fn parse(input: ParseStream<'_>) -> Result<Self> {
269 if input.peek(LitStr) {
270 return input.parse().map(Self::Json);
271 }
272 input.parse().map(Self::Object)
273 }
274}
275
276struct OpenApiObject {
277 value: Map<String, Value>,
278 span: proc_macro2::Span,
279}
280
281impl Parse for OpenApiObject {
282 fn parse(input: ParseStream<'_>) -> Result<Self> {
283 let content;
284 let brace = braced!(content in input);
285 Ok(Self {
286 value: parse_object_entries(&content)?,
287 span: brace.span.join(),
288 })
289 }
290}
291
292fn parse_object_entries(input: ParseStream<'_>) -> Result<Map<String, Value>> {
293 let mut object = Map::new();
294 while !input.is_empty() {
295 let (key, span) = parse_object_key(input)?;
296 input.parse::<Token![:]>()?;
297 let value = parse_json_value(input)?;
298 if object.insert(key.clone(), value).is_some() {
299 return Err(Error::new(
300 span,
301 format!("duplicate OpenAPI object key `{key}`"),
302 ));
303 }
304 if input.is_empty() {
305 break;
306 }
307 input.parse::<Token![,]>()?;
308 }
309 Ok(object)
310}
311
312fn parse_object_key(input: ParseStream<'_>) -> Result<(String, proc_macro2::Span)> {
313 if input.peek(LitStr) {
314 let key = input.parse::<LitStr>()?;
315 return Ok((key.value(), key.span()));
316 }
317 let key = Ident::parse_any(input)?;
318 Ok((key.unraw().to_string(), key.span()))
319}
320
321fn parse_json_value(input: ParseStream<'_>) -> Result<Value> {
322 if input.peek(syn::token::Brace) {
323 let content;
324 braced!(content in input);
325 return parse_object_entries(&content).map(Value::Object);
326 }
327 if input.peek(syn::token::Bracket) {
328 let content;
329 bracketed!(content in input);
330 let values = Punctuated::<JsonValue, Token![,]>::parse_terminated(&content)?;
331 return Ok(Value::Array(
332 values.into_iter().map(|value| value.0).collect(),
333 ));
334 }
335 if input.peek(Token![-]) {
336 input.parse::<Token![-]>()?;
337 let literal = input.parse::<Lit>()?;
338 return parse_number_literal(&literal, true);
339 }
340 if input.peek(syn::LitBool) {
341 let literal = input.parse::<Lit>()?;
342 return parse_literal(&literal);
343 }
344 if input.peek(Ident::peek_any) {
345 let ident = Ident::parse_any(input)?;
346 if ident == "null" {
347 return Ok(Value::Null);
348 }
349 return Err(Error::new(ident.span(), "expected a JSON value"));
350 }
351 let literal = input.parse::<Lit>()?;
352 parse_literal(&literal)
353}
354
355struct JsonValue(Value);
356
357impl Parse for JsonValue {
358 fn parse(input: ParseStream<'_>) -> Result<Self> {
359 parse_json_value(input).map(Self)
360 }
361}
362
363fn parse_literal(literal: &Lit) -> Result<Value> {
364 match literal {
365 Lit::Str(value) => Ok(Value::String(value.value())),
366 Lit::Bool(value) => Ok(Value::Bool(value.value)),
367 Lit::Int(_) | Lit::Float(_) => parse_number_literal(literal, false),
368 _ => Err(Error::new(
369 literal.span(),
370 "OpenAPI metadata supports JSON string, number, boolean, null, array, and object values",
371 )),
372 }
373}
374
375fn parse_number_literal(literal: &Lit, negative: bool) -> Result<Value> {
376 let mut source = literal.to_token_stream().to_string().replace(' ', "");
377 if negative {
378 source.insert(0, '-');
379 }
380 let number = source.parse::<Number>().map_err(|_| {
381 Error::new(
382 literal.span(),
383 "OpenAPI numeric values must be unsuffixed JSON numbers",
384 )
385 })?;
386 Ok(Value::Number(number))
387}
388
389fn operation_literal(operation: &OpenApiObject) -> Result<LitStr> {
390 if operation.value.contains_key("operationId") {
391 return Err(Error::new(
392 operation.span,
393 "OpenAPI operationId is generated from the stable route ID",
394 ));
395 }
396 let json = serde_json::to_string(&operation.value).map_err(|error| {
397 Error::new(
398 operation.span,
399 format!("could not encode OpenAPI Operation Object: {error}"),
400 )
401 })?;
402 Ok(LitStr::new(&json, operation.span))
403}
404
405fn validate_openapi_operation(operation: &LitStr) -> Result<()> {
406 let value = serde_json::from_str::<serde_json::Value>(&operation.value()).map_err(|error| {
407 Error::new(
408 operation.span(),
409 format!("OpenAPI Operation Object is not valid JSON: {error}"),
410 )
411 })?;
412 let Some(object) = value.as_object() else {
413 return Err(Error::new(
414 operation.span(),
415 "OpenAPI operation metadata must be a JSON object",
416 ));
417 };
418 if object.contains_key("operationId") {
419 return Err(Error::new(
420 operation.span(),
421 "OpenAPI operationId is generated from the stable route ID",
422 ));
423 }
424 Ok(())
425}
426
427fn handler_arguments(method: &syn::ImplItemFn) -> Result<Vec<HandlerArgument>> {
428 let mut arguments = Vec::new();
429 let mut context_count = 0;
430 let mut request_count = 0;
431 for (index, input) in method.sig.inputs.iter().enumerate() {
432 let FnArg::Typed(argument) = input else {
433 continue;
434 };
435 let ty = (*argument.ty).clone();
436 let kind = match final_type_ident(&ty).map(Ident::to_string).as_deref() {
437 Some("InvocationContext") => {
438 context_count += 1;
439 ArgumentKind::Context
440 }
441 Some("HandleRequest") => {
442 request_count += 1;
443 ArgumentKind::Request
444 }
445 _ => ArgumentKind::Extractor(Ident::new(
446 &format!("__lenso_extracted_{index}"),
447 argument.span(),
448 )),
449 };
450 arguments.push(HandlerArgument { ty, kind });
451 }
452 if context_count > 1 || request_count > 1 {
453 return Err(Error::new_spanned(
454 &method.sig.inputs,
455 "an endpoint handler accepts at most one InvocationContext and one HandleRequest",
456 ));
457 }
458 Ok(arguments)
459}
460
461fn final_type_ident(ty: &Type) -> Option<&Ident> {
462 let Type::Path(path) = ty else {
463 return None;
464 };
465 path.path.segments.last().map(|segment| &segment.ident)
466}
467
468fn dispatch_arm(handler: &Handler) -> proc_macro2::TokenStream {
469 let route_id = &handler.route.id;
470 let method = &handler.method;
471 let middleware_steps = handler.middlewares.iter().map(|middleware| {
472 quote! {
473 let (context, request) = match provider.#middleware(context, request).await {
474 Ok(outcome) => match outcome.into_result() {
475 Ok(next) => next,
476 Err(response) => return Ok(Ok(response)),
477 },
478 Err(::lenso_capability_http_endpoint::EndpointHandleInvocationError::Domain(
479 error,
480 )) => return Ok(Err(error)),
481 Err(::lenso_capability_http_endpoint::EndpointHandleInvocationError::Runtime(
482 error,
483 )) => return Err(error),
484 };
485 }
486 });
487 let extractor_steps = handler.arguments.iter().filter_map(|argument| {
488 let ArgumentKind::Extractor(binding) = &argument.kind else {
489 return None;
490 };
491 let ty = &argument.ty;
492 Some(quote! {
493 let #binding: #ty = match <#ty as
494 ::lenso_capability_http_endpoint::__private::FromRequest<Self>
495 >::from_request(&provider, &mut context, &request).await {
496 Ok(value) => value,
497 Err(::lenso_capability_http_endpoint::__private::ExtractorRejection::Response(
498 response,
499 )) => return Ok(Ok(response)),
500 Err(::lenso_capability_http_endpoint::__private::ExtractorRejection::Invocation(
501 ::lenso_capability_http_endpoint::EndpointHandleInvocationError::Domain(
502 error,
503 ),
504 )) => return Ok(Err(error)),
505 Err(::lenso_capability_http_endpoint::__private::ExtractorRejection::Invocation(
506 ::lenso_capability_http_endpoint::EndpointHandleInvocationError::Runtime(
507 error,
508 ),
509 )) => return Err(error),
510 };
511 })
512 });
513 let mutable_context = handler
514 .arguments
515 .iter()
516 .any(|argument| matches!(&argument.kind, ArgumentKind::Extractor(_)))
517 .then(|| quote!(let mut context = context;));
518 let arguments = handler
519 .arguments
520 .iter()
521 .map(|argument| match &argument.kind {
522 ArgumentKind::Context => quote!(context),
523 ArgumentKind::Request => quote!(request),
524 ArgumentKind::Extractor(binding) => quote!(#binding),
525 });
526
527 quote! {
528 #route_id => {
529 #(#middleware_steps)*
530 #mutable_context
531 #(#extractor_steps)*
532 let _ = (&context, &request);
533 ::lenso_capability_http_endpoint::__private::IntoEndpointResult::into_endpoint_result(
534 provider.#method(#(#arguments),*).await,
535 )
536 }
537 }
538}
539
540fn http_method(attribute: &Attribute) -> Option<&'static str> {
541 let path = attribute.path();
542 [
543 ("get", "GET"),
544 ("post", "POST"),
545 ("put", "PUT"),
546 ("patch", "PATCH"),
547 ("delete", "DELETE"),
548 ("head", "HEAD"),
549 ("options", "OPTIONS"),
550 ("query", "QUERY"),
551 ]
552 .into_iter()
553 .find_map(|(attribute, method)| path.is_ident(attribute).then_some(method))
554}
555
556struct RouteArguments {
557 route_id: LitStr,
558 path: LitStr,
559}
560
561impl Parse for RouteArguments {
562 fn parse(input: ParseStream<'_>) -> Result<Self> {
563 let arguments = Punctuated::<LitStr, Token![,]>::parse_terminated(input)?;
564 if arguments.len() != 2 {
565 return Err(Error::new(
566 input.span(),
567 "HTTP handler attributes require a route ID and path",
568 ));
569 }
570 let mut arguments = arguments.into_iter();
571 Ok(Self {
572 route_id: arguments.next().expect("length was checked"),
573 path: arguments.next().expect("length was checked"),
574 })
575 }
576}
577
578struct Route {
579 method: LitStr,
580 id: LitStr,
581 path: LitStr,
582 openapi: Option<LitStr>,
583}
584
585struct HandlerMetadata {
586 route: Option<Route>,
587 middlewares: Vec<Ident>,
588 openapi: Option<LitStr>,
589}
590
591struct Handler {
592 route: Route,
593 middlewares: Vec<Ident>,
594 method: Ident,
595 arguments: Vec<HandlerArgument>,
596}
597
598struct HandlerArgument {
599 ty: Type,
600 kind: ArgumentKind,
601}
602
603enum ArgumentKind {
604 Context,
605 Request,
606 Extractor(Ident),
607}
608
609#[cfg(test)]
610mod tests {
611 use super::expand_endpoint;
612 use syn::parse_quote;
613
614 #[test]
615 fn expands_handler_attributes_into_the_static_route_table() {
616 let expanded = expand_endpoint(parse_quote! {
617 impl OrdersHttp {
618 #[get("orders.read", "/orders/{order_id}")]
619 #[openapi({
620 summary: "Read an order",
621 tags: ["orders"],
622 responses: {
623 "200": { description: "Order" }
624 }
625 })]
626 async fn read(&self) {}
627 }
628 })
629 .unwrap()
630 .to_string();
631
632 assert!(expanded.contains("HttpEndpoint"));
633 assert!(expanded.contains("orders.read"));
634 assert!(expanded.contains("GET"));
635 assert!(expanded.contains("/orders/{order_id}"));
636 assert!(expanded.contains("with_openapi"));
637 assert!(!expanded.contains("# [get"));
638 assert!(!expanded.contains("# [openapi"));
639 assert!(expanded.contains("Read an order"));
640 assert!(expanded.contains(r#"\"200\""#));
641 }
642
643 #[test]
644 fn accepts_legacy_json_string_metadata() {
645 let expanded = expand_endpoint(parse_quote! {
646 impl OrdersHttp {
647 #[get("orders.read", "/orders/{order_id}")]
648 #[openapi(r#"{"summary":"Read an order"}"#)]
649 async fn read(&self) {}
650 }
651 })
652 .unwrap()
653 .to_string();
654
655 assert!(expanded.contains("Read an order"));
656 }
657
658 #[test]
659 fn rejects_an_operation_id_in_structured_metadata() {
660 let error = expand_endpoint(parse_quote! {
661 impl OrdersHttp {
662 #[get("orders.read", "/orders/{order_id}")]
663 #[openapi({ operationId: "another.id" })]
664 async fn read(&self) {}
665 }
666 })
667 .unwrap_err();
668
669 assert!(
670 error
671 .to_string()
672 .contains("generated from the stable route ID")
673 );
674 }
675
676 #[test]
677 fn rejects_duplicate_structured_metadata_keys() {
678 let error = expand_endpoint(parse_quote! {
679 impl OrdersHttp {
680 #[get("orders.read", "/orders/{order_id}")]
681 #[openapi({ summary: "Read", summary: "Read again" })]
682 async fn read(&self) {}
683 }
684 })
685 .unwrap_err();
686
687 assert!(error.to_string().contains("duplicate OpenAPI object key"));
688 }
689
690 #[test]
691 fn rejects_an_openapi_operation_id_that_can_drift_from_the_route_id() {
692 let error = expand_endpoint(parse_quote! {
693 impl OrdersHttp {
694 #[get("orders.read", "/orders/{order_id}")]
695 #[openapi(r#"{"operationId":"another.id"}"#)]
696 async fn read(&self) {}
697 }
698 })
699 .unwrap_err();
700
701 assert!(
702 error
703 .to_string()
704 .contains("generated from the stable route ID")
705 );
706 }
707
708 #[test]
709 fn rejects_invalid_openapi_json() {
710 let error = expand_endpoint(parse_quote! {
711 impl OrdersHttp {
712 #[get("orders.read", "/orders/{order_id}")]
713 #[openapi("not-json")]
714 async fn read(&self) {}
715 }
716 })
717 .unwrap_err();
718
719 assert!(error.to_string().contains("not valid JSON"));
720 }
721
722 #[test]
723 fn expands_middleware_and_typed_extractors_before_the_handler() {
724 let expanded = expand_endpoint(parse_quote! {
725 impl OrdersHttp {
726 #[middleware(authenticate)]
727 #[get("orders.read", "/orders/{order_id}")]
728 async fn read(
729 &self,
730 context: InvocationContext,
731 Path(path): Path<OrderPath>,
732 ) {}
733 }
734 })
735 .unwrap()
736 .to_string();
737
738 assert!(expanded.contains("provider . authenticate"));
739 assert!(expanded.contains("into_result"));
740 assert!(expanded.contains("FromRequest"));
741 assert!(expanded.contains("provider . read (context , __lenso_extracted_2)"));
742 assert!(!expanded.contains("# [middleware"));
743 }
744
745 #[test]
746 fn applies_provider_middleware_before_route_middleware() {
747 let expanded = expand_endpoint(parse_quote! {
748 #[middleware(trace_all)]
749 impl OrdersHttp {
750 #[middleware(authorize_read)]
751 #[get("orders.read", "/orders/{order_id}")]
752 async fn read(&self) {}
753
754 #[get("orders.list", "/orders")]
755 async fn list(&self) {}
756 }
757 })
758 .unwrap()
759 .to_string();
760
761 assert_eq!(expanded.matches("provider . trace_all").count(), 2);
762 assert_eq!(expanded.matches("provider . authorize_read").count(), 1);
763 assert!(
764 expanded.find("provider . trace_all").unwrap()
765 < expanded.find("provider . authorize_read").unwrap()
766 );
767 assert!(!expanded.contains("# [middleware"));
768 }
769
770 #[test]
771 fn rejects_handlers_with_multiple_http_methods() {
772 let error = expand_endpoint(parse_quote! {
773 impl OrdersHttp {
774 #[get("orders.read", "/orders/{order_id}")]
775 #[post("orders.read", "/orders/{order_id}")]
776 async fn read(&self) {}
777 }
778 })
779 .unwrap_err();
780
781 assert!(error.to_string().contains("only one HTTP method"));
782 }
783
784 #[test]
785 fn rejects_impls_without_handlers() {
786 let error = expand_endpoint(parse_quote! {
787 impl OrdersHttp {
788 fn helper(&self) {}
789 }
790 })
791 .unwrap_err();
792
793 assert!(error.to_string().contains("at least one HTTP handler"));
794 }
795}