StructuredSwift+Server.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. extension FunctionSignatureDescription {
  17. /// ```
  18. /// func <Method>(
  19. /// request: GRPCCore.ServerRequest<Input>,
  20. /// context: GRPCCore.ServerContext
  21. /// ) async throws -> GRPCCore.ServerResponse<Output>
  22. /// ```
  23. static func serverMethod(
  24. accessLevel: AccessModifier? = nil,
  25. name: String,
  26. input: String,
  27. output: String,
  28. streamingInput: Bool,
  29. streamingOutput: Bool
  30. ) -> Self {
  31. return FunctionSignatureDescription(
  32. accessModifier: accessLevel,
  33. kind: .function(name: name),
  34. parameters: [
  35. ParameterDescription(
  36. label: "request",
  37. type: .serverRequest(forType: input, streaming: streamingInput)
  38. ),
  39. ParameterDescription(label: "context", type: .serverContext),
  40. ],
  41. keywords: [.async, .throws],
  42. returnType: .identifierType(.serverResponse(forType: output, streaming: streamingOutput))
  43. )
  44. }
  45. }
  46. extension ProtocolDescription {
  47. /// ```
  48. /// protocol <Name>: GRPCCore.RegistrableRPCService {
  49. /// ...
  50. /// }
  51. /// ```
  52. static func streamingService(
  53. accessLevel: AccessModifier? = nil,
  54. name: String,
  55. methods: [MethodDescriptor]
  56. ) -> Self {
  57. func docs(for method: MethodDescriptor) -> String {
  58. let summary = """
  59. /// Handle the "\(method.name.identifyingName)" method.
  60. """
  61. let parameters = """
  62. /// - Parameters:
  63. /// - request: A streaming request of `\(method.inputType)` messages.
  64. /// - context: Context providing information about the RPC.
  65. /// - Throws: Any error which occurred during the processing of the request. Thrown errors
  66. /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
  67. /// to an internal error.
  68. /// - Returns: A streaming response of `\(method.outputType)` messages.
  69. """
  70. return Docs.interposeDocs(method.documentation, between: summary, and: parameters)
  71. }
  72. return ProtocolDescription(
  73. accessModifier: accessLevel,
  74. name: name,
  75. conformances: ["GRPCCore.RegistrableRPCService"],
  76. members: methods.map { method in
  77. .commentable(
  78. .preFormatted(docs(for: method)),
  79. .function(
  80. signature: .serverMethod(
  81. name: method.name.functionName,
  82. input: method.inputType,
  83. output: method.outputType,
  84. streamingInput: true,
  85. streamingOutput: true
  86. )
  87. )
  88. )
  89. }
  90. )
  91. }
  92. }
  93. extension ExtensionDescription {
  94. /// ```
  95. /// extension <ExtensionName> {
  96. /// func registerMethods(with router: inout GRPCCore.RPCRouter) {
  97. /// // ...
  98. /// }
  99. /// }
  100. /// ```
  101. static func registrableRPCServiceDefaultImplementation(
  102. accessLevel: AccessModifier? = nil,
  103. on extensionName: String,
  104. serviceNamespace: String,
  105. methods: [MethodDescriptor],
  106. serializer: (String) -> String,
  107. deserializer: (String) -> String
  108. ) -> Self {
  109. return ExtensionDescription(
  110. onType: extensionName,
  111. declarations: [
  112. .function(
  113. .registerMethods(
  114. accessLevel: accessLevel,
  115. serviceNamespace: serviceNamespace,
  116. methods: methods,
  117. serializer: serializer,
  118. deserializer: deserializer
  119. )
  120. )
  121. ]
  122. )
  123. }
  124. }
  125. extension ProtocolDescription {
  126. /// ```
  127. /// protocol <Name>: <StreamingProtocol> {
  128. /// ...
  129. /// }
  130. /// ```
  131. static func service(
  132. accessLevel: AccessModifier? = nil,
  133. name: String,
  134. streamingProtocol: String,
  135. methods: [MethodDescriptor]
  136. ) -> Self {
  137. func docs(for method: MethodDescriptor) -> String {
  138. let summary = """
  139. /// Handle the "\(method.name.identifyingName)" method.
  140. """
  141. let request: String
  142. if method.isInputStreaming {
  143. request = "A streaming request of `\(method.inputType)` messages."
  144. } else {
  145. request = "A request containing a single `\(method.inputType)` message."
  146. }
  147. let returns: String
  148. if method.isOutputStreaming {
  149. returns = "A streaming response of `\(method.outputType)` messages."
  150. } else {
  151. returns = "A response containing a single `\(method.outputType)` message."
  152. }
  153. let parameters = """
  154. /// - Parameters:
  155. /// - request: \(request)
  156. /// - context: Context providing information about the RPC.
  157. /// - Throws: Any error which occurred during the processing of the request. Thrown errors
  158. /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
  159. /// to an internal error.
  160. /// - Returns: \(returns)
  161. """
  162. return Docs.interposeDocs(method.documentation, between: summary, and: parameters)
  163. }
  164. return ProtocolDescription(
  165. accessModifier: accessLevel,
  166. name: name,
  167. conformances: [streamingProtocol],
  168. members: methods.map { method in
  169. .commentable(
  170. .preFormatted(docs(for: method)),
  171. .function(
  172. signature: .serverMethod(
  173. name: method.name.functionName,
  174. input: method.inputType,
  175. output: method.outputType,
  176. streamingInput: method.isInputStreaming,
  177. streamingOutput: method.isOutputStreaming
  178. )
  179. )
  180. )
  181. }
  182. )
  183. }
  184. }
  185. extension FunctionCallDescription {
  186. /// ```
  187. /// self.<Name>(request: request, context: context)
  188. /// ```
  189. static func serverMethodCallOnSelf(
  190. name: String,
  191. requestArgument: Expression = .identifierPattern("request")
  192. ) -> Self {
  193. return FunctionCallDescription(
  194. calledExpression: .memberAccess(
  195. MemberAccessDescription(
  196. left: .identifierPattern("self"),
  197. right: name
  198. )
  199. ),
  200. arguments: [
  201. FunctionArgumentDescription(
  202. label: "request",
  203. expression: requestArgument
  204. ),
  205. FunctionArgumentDescription(
  206. label: "context",
  207. expression: .identifierPattern("context")
  208. ),
  209. ]
  210. )
  211. }
  212. }
  213. extension ClosureInvocationDescription {
  214. /// ```
  215. /// { router, context in
  216. /// try await self.<Method>(
  217. /// request: request,
  218. /// context: context
  219. /// )
  220. /// }
  221. /// ```
  222. static func routerHandlerInvokingRPC(method: String) -> Self {
  223. return ClosureInvocationDescription(
  224. argumentNames: ["request", "context"],
  225. body: [
  226. .expression(
  227. .unaryKeyword(
  228. kind: .try,
  229. expression: .unaryKeyword(
  230. kind: .await,
  231. expression: .functionCall(.serverMethodCallOnSelf(name: method))
  232. )
  233. )
  234. )
  235. ]
  236. )
  237. }
  238. }
  239. /// ```
  240. /// router.registerHandler(
  241. /// forMethod: ...,
  242. /// deserializer: ...
  243. /// serializer: ...
  244. /// handler: { request, context in
  245. /// // ...
  246. /// }
  247. /// )
  248. /// ```
  249. extension FunctionCallDescription {
  250. static func registerWithRouter(
  251. serviceNamespace: String,
  252. methodNamespace: String,
  253. methodName: String,
  254. inputDeserializer: String,
  255. outputSerializer: String
  256. ) -> Self {
  257. return FunctionCallDescription(
  258. calledExpression: .memberAccess(
  259. .init(left: .identifierPattern("router"), right: "registerHandler")
  260. ),
  261. arguments: [
  262. FunctionArgumentDescription(
  263. label: "forMethod",
  264. expression: .identifierPattern("\(serviceNamespace).Method.\(methodNamespace).descriptor")
  265. ),
  266. FunctionArgumentDescription(
  267. label: "deserializer",
  268. expression: .identifierPattern(inputDeserializer)
  269. ),
  270. FunctionArgumentDescription(
  271. label: "serializer",
  272. expression: .identifierPattern(outputSerializer)
  273. ),
  274. FunctionArgumentDescription(
  275. label: "handler",
  276. expression: .closureInvocation(.routerHandlerInvokingRPC(method: methodName))
  277. ),
  278. ]
  279. )
  280. }
  281. }
  282. extension FunctionDescription {
  283. /// ```
  284. /// func registerMethods(with router: inout GRPCCore.RPCRouter) {
  285. /// // ...
  286. /// }
  287. /// ```
  288. static func registerMethods(
  289. accessLevel: AccessModifier? = nil,
  290. serviceNamespace: String,
  291. methods: [MethodDescriptor],
  292. serializer: (String) -> String,
  293. deserializer: (String) -> String
  294. ) -> Self {
  295. return FunctionDescription(
  296. accessModifier: accessLevel,
  297. kind: .function(name: "registerMethods"),
  298. generics: [.member("Transport")],
  299. parameters: [
  300. ParameterDescription(
  301. label: "with",
  302. name: "router",
  303. type: .rpcRouter(genericOver: "Transport"),
  304. `inout`: true
  305. )
  306. ],
  307. whereClause: WhereClause(
  308. requirements: [
  309. .conformance("Transport", "GRPCCore.ServerTransport")
  310. ]
  311. ),
  312. body: methods.map { method in
  313. .functionCall(
  314. .registerWithRouter(
  315. serviceNamespace: serviceNamespace,
  316. methodNamespace: method.name.typeName,
  317. methodName: method.name.functionName,
  318. inputDeserializer: deserializer(method.inputType),
  319. outputSerializer: serializer(method.outputType)
  320. )
  321. )
  322. }
  323. )
  324. }
  325. }
  326. extension FunctionDescription {
  327. /// ```
  328. /// func <Name>(
  329. /// request: GRPCCore.StreamingServerRequest<Input>
  330. /// context: GRPCCore.ServerContext
  331. /// ) async throws -> GRPCCore.StreamingServerResponse<Output> {
  332. /// let response = try await self.<Name>(
  333. /// request: GRPCCore.ServerRequest(stream: request),
  334. /// context: context
  335. /// )
  336. /// return GRPCCore.StreamingServerResponse(single: response)
  337. /// }
  338. /// ```
  339. static func serverStreamingMethodsCallingMethod(
  340. accessLevel: AccessModifier? = nil,
  341. name: String,
  342. input: String,
  343. output: String,
  344. streamingInput: Bool,
  345. streamingOutput: Bool
  346. ) -> FunctionDescription {
  347. let signature: FunctionSignatureDescription = .serverMethod(
  348. accessLevel: accessLevel,
  349. name: name,
  350. input: input,
  351. output: output,
  352. // This method converts from the fully streamed version to the specified version.
  353. streamingInput: true,
  354. streamingOutput: true
  355. )
  356. // Call the underlying function.
  357. let functionCall: Expression = .functionCall(
  358. calledExpression: .memberAccess(
  359. MemberAccessDescription(
  360. left: .identifierPattern("self"),
  361. right: name
  362. )
  363. ),
  364. arguments: [
  365. FunctionArgumentDescription(
  366. label: "request",
  367. expression: streamingInput
  368. ? .identifierPattern("request")
  369. : .functionCall(
  370. calledExpression: .identifierType(.serverRequest(forType: nil, streaming: false)),
  371. arguments: [
  372. FunctionArgumentDescription(
  373. label: "stream",
  374. expression: .identifierPattern("request")
  375. )
  376. ]
  377. )
  378. ),
  379. FunctionArgumentDescription(
  380. label: "context",
  381. expression: .identifierPattern("context")
  382. ),
  383. ]
  384. )
  385. // Call the function and assign to 'response'.
  386. let response: Declaration = .variable(
  387. kind: .let,
  388. left: "response",
  389. right: .unaryKeyword(
  390. kind: .try,
  391. expression: .unaryKeyword(
  392. kind: .await,
  393. expression: functionCall
  394. )
  395. )
  396. )
  397. // Build the return statement.
  398. let returnExpression: Expression = .unaryKeyword(
  399. kind: .return,
  400. expression: streamingOutput
  401. ? .identifierPattern("response")
  402. : .functionCall(
  403. calledExpression: .identifierType(.serverResponse(forType: nil, streaming: true)),
  404. arguments: [
  405. FunctionArgumentDescription(
  406. label: "single",
  407. expression: .identifierPattern("response")
  408. )
  409. ]
  410. )
  411. )
  412. return Self(
  413. signature: signature,
  414. body: [.declaration(response), .expression(returnExpression)]
  415. )
  416. }
  417. }
  418. extension ExtensionDescription {
  419. /// ```
  420. /// extension <ExtensionName> {
  421. /// func <Name>(
  422. /// request: GRPCCore.StreamingServerRequest<Input>
  423. /// context: GRPCCore.ServerContext
  424. /// ) async throws -> GRPCCore.StreamingServerResponse<Output> {
  425. /// let response = try await self.<Name>(
  426. /// request: GRPCCore.ServerRequest(stream: request),
  427. /// context: context
  428. /// )
  429. /// return GRPCCore.StreamingServerResponse(single: response)
  430. /// }
  431. /// ...
  432. /// }
  433. /// ```
  434. static func streamingServiceProtocolDefaultImplementation(
  435. accessModifier: AccessModifier? = nil,
  436. on extensionName: String,
  437. methods: [MethodDescriptor]
  438. ) -> Self {
  439. return ExtensionDescription(
  440. onType: extensionName,
  441. declarations: methods.compactMap { method -> Declaration? in
  442. // Bidirectional streaming methods don't need a default implementation as their signatures
  443. // match across the two protocols.
  444. if method.isInputStreaming, method.isOutputStreaming { return nil }
  445. return .function(
  446. .serverStreamingMethodsCallingMethod(
  447. accessLevel: accessModifier,
  448. name: method.name.functionName,
  449. input: method.inputType,
  450. output: method.outputType,
  451. streamingInput: method.isInputStreaming,
  452. streamingOutput: method.isOutputStreaming
  453. )
  454. )
  455. }
  456. )
  457. }
  458. }
  459. extension FunctionSignatureDescription {
  460. /// ```
  461. /// func <Name>(
  462. /// request: <Input>,
  463. /// context: GRPCCore.ServerContext,
  464. /// ) async throws -> <Output>
  465. /// ```
  466. ///
  467. /// ```
  468. /// func <Name>(
  469. /// request: GRPCCore.RPCAsyncSequence<Input, any Error>,
  470. /// response: GRPCCore.RPCAsyncWriter<Output>
  471. /// context: GRPCCore.ServerContext,
  472. /// ) async throws
  473. /// ```
  474. static func simpleServerMethod(
  475. accessLevel: AccessModifier? = nil,
  476. name: String,
  477. input: String,
  478. output: String,
  479. streamingInput: Bool,
  480. streamingOutput: Bool
  481. ) -> Self {
  482. var parameters: [ParameterDescription] = [
  483. ParameterDescription(
  484. label: "request",
  485. type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input)
  486. )
  487. ]
  488. if streamingOutput {
  489. parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output)))
  490. }
  491. parameters.append(ParameterDescription(label: "context", type: .serverContext))
  492. return FunctionSignatureDescription(
  493. accessModifier: accessLevel,
  494. kind: .function(name: name),
  495. parameters: parameters,
  496. keywords: [.async, .throws],
  497. returnType: streamingOutput ? nil : .identifier(.pattern(output))
  498. )
  499. }
  500. }
  501. extension ProtocolDescription {
  502. /// ```
  503. /// protocol SimpleServiceProtocol: <ServiceProtocol> {
  504. /// ...
  505. /// }
  506. /// ```
  507. static func simpleServiceProtocol(
  508. accessModifier: AccessModifier? = nil,
  509. name: String,
  510. serviceProtocol: String,
  511. methods: [MethodDescriptor]
  512. ) -> Self {
  513. func docs(for method: MethodDescriptor) -> String {
  514. let summary = """
  515. /// Handle the "\(method.name.identifyingName)" method.
  516. """
  517. let requestText =
  518. method.isInputStreaming
  519. ? "A stream of `\(method.inputType)` messages."
  520. : "A `\(method.inputType)` message."
  521. var parameters = """
  522. /// - Parameters:
  523. /// - request: \(requestText)
  524. """
  525. if method.isOutputStreaming {
  526. parameters += "\n"
  527. parameters += """
  528. /// - response: A response stream of `\(method.outputType)` messages.
  529. """
  530. }
  531. parameters += "\n"
  532. parameters += """
  533. /// - context: Context providing information about the RPC.
  534. /// - Throws: Any error which occurred during the processing of the request. Thrown errors
  535. /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
  536. /// to an internal error.
  537. """
  538. if !method.isOutputStreaming {
  539. parameters += "\n"
  540. parameters += """
  541. /// - Returns: A `\(method.outputType)` to respond with.
  542. """
  543. }
  544. return Docs.interposeDocs(method.documentation, between: summary, and: parameters)
  545. }
  546. return ProtocolDescription(
  547. accessModifier: accessModifier,
  548. name: name,
  549. conformances: [serviceProtocol],
  550. members: methods.map { method in
  551. .commentable(
  552. .preFormatted(docs(for: method)),
  553. .function(
  554. signature: .simpleServerMethod(
  555. name: method.name.functionName,
  556. input: method.inputType,
  557. output: method.outputType,
  558. streamingInput: method.isInputStreaming,
  559. streamingOutput: method.isOutputStreaming
  560. )
  561. )
  562. )
  563. }
  564. )
  565. }
  566. }
  567. extension FunctionCallDescription {
  568. /// ```
  569. /// try await self.<Name>(
  570. /// request: request.message,
  571. /// response: writer,
  572. /// context: context
  573. /// )
  574. /// ```
  575. static func serviceMethodCallingSimpleMethod(
  576. name: String,
  577. input: String,
  578. output: String,
  579. streamingInput: Bool,
  580. streamingOutput: Bool
  581. ) -> Self {
  582. var arguments: [FunctionArgumentDescription] = [
  583. FunctionArgumentDescription(
  584. label: "request",
  585. expression: .identifierPattern("request").dot(streamingInput ? "messages" : "message")
  586. )
  587. ]
  588. if streamingOutput {
  589. arguments.append(
  590. FunctionArgumentDescription(
  591. label: "response",
  592. expression: .identifierPattern("writer")
  593. )
  594. )
  595. }
  596. arguments.append(
  597. FunctionArgumentDescription(
  598. label: "context",
  599. expression: .identifierPattern("context")
  600. )
  601. )
  602. return FunctionCallDescription(
  603. calledExpression: .try(.await(.identifierPattern("self").dot(name))),
  604. arguments: arguments
  605. )
  606. }
  607. }
  608. extension FunctionDescription {
  609. /// ```
  610. /// func <Name>(
  611. /// request: GRPCCore.ServerRequest<Input>,
  612. /// context: GRPCCore.ServerContext
  613. /// ) async throws -> GRPCCore.ServerResponse<Output> {
  614. /// return GRPCCore.ServerResponse<Output>(
  615. /// message: try await self.<Name>(
  616. /// request: request.message,
  617. /// context: context
  618. /// )
  619. /// metadata: [:]
  620. /// )
  621. /// }
  622. /// ```
  623. static func serviceProtocolDefaultImplementation(
  624. accessModifier: AccessModifier? = nil,
  625. name: String,
  626. input: String,
  627. output: String,
  628. streamingInput: Bool,
  629. streamingOutput: Bool
  630. ) -> Self {
  631. func makeUnaryOutputArguments() -> [FunctionArgumentDescription] {
  632. return [
  633. FunctionArgumentDescription(
  634. label: "message",
  635. expression: .functionCall(
  636. .serviceMethodCallingSimpleMethod(
  637. name: name,
  638. input: input,
  639. output: output,
  640. streamingInput: streamingInput,
  641. streamingOutput: streamingOutput
  642. )
  643. )
  644. ),
  645. FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
  646. ]
  647. }
  648. func makeStreamingOutputArguments() -> [FunctionArgumentDescription] {
  649. return [
  650. FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
  651. FunctionArgumentDescription(
  652. label: "producer",
  653. expression: .closureInvocation(
  654. argumentNames: ["writer"],
  655. body: [
  656. .expression(
  657. .functionCall(
  658. .serviceMethodCallingSimpleMethod(
  659. name: name,
  660. input: input,
  661. output: output,
  662. streamingInput: streamingInput,
  663. streamingOutput: streamingOutput
  664. )
  665. )
  666. ),
  667. .expression(.return(.literal(.dictionary([])))),
  668. ]
  669. )
  670. ),
  671. ]
  672. }
  673. return FunctionDescription(
  674. signature: .serverMethod(
  675. accessLevel: accessModifier,
  676. name: name,
  677. input: input,
  678. output: output,
  679. streamingInput: streamingInput,
  680. streamingOutput: streamingOutput
  681. ),
  682. body: [
  683. .expression(
  684. .functionCall(
  685. calledExpression: .return(
  686. .identifierType(
  687. .serverResponse(forType: output, streaming: streamingOutput)
  688. )
  689. ),
  690. arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments()
  691. )
  692. )
  693. ]
  694. )
  695. }
  696. }
  697. extension ExtensionDescription {
  698. /// ```
  699. /// extension ServiceProtocol {
  700. /// ...
  701. /// }
  702. /// ```
  703. static func serviceProtocolDefaultImplementation(
  704. accessModifier: AccessModifier? = nil,
  705. on extensionName: String,
  706. methods: [MethodDescriptor]
  707. ) -> Self {
  708. ExtensionDescription(
  709. onType: extensionName,
  710. declarations: methods.map { method in
  711. .function(
  712. .serviceProtocolDefaultImplementation(
  713. accessModifier: accessModifier,
  714. name: method.name.functionName,
  715. input: method.inputType,
  716. output: method.outputType,
  717. streamingInput: method.isInputStreaming,
  718. streamingOutput: method.isOutputStreaming
  719. )
  720. )
  721. }
  722. )
  723. }
  724. }