@@ -33,7 +33,8 @@ import code.api.dynamic.endpoint.helper.practise.{DynamicEndpointCodeGenerator,
3333import code .api .dynamic .endpoint .helper .practise .PractiseEndpointGroup
3434import code .api .util .DynamicUtil .{DynamicCodeBody , Validation }
3535import code .api .util .APIUtil .{BooleanBody , DoubleBody , EmptyBody , LongBody , Http4sEndpointIO , PrimaryDataBody , ResourceDoc , StringBody , getDisabledEndpointOperationIds }
36- import code .api .util .{APIUtil , CallContext , DynamicUtil }
36+ import code .api .util .{APIUtil , CallContext , DynamicUtil , ErrorMessages }
37+ import code .api .JsonResponseException
3738import net .liftweb .common .{Box , Failure , Full }
3839import org .json4s .{JNothing , JValue }
3940import org .json4s .JsonAST .{JBool , JDouble , JInt , JString }
@@ -111,10 +112,23 @@ object CompiledObjects {
111112 * validate and the dry-run compile all check against it, and CompiledObjects chooses its compiler
112113 * from the same normalised value, so nothing that passes the check can reach the wrong compiler.
113114 */
114- val supportedLanguages : List [String ] = List (" scala" , " java" )
115+ val supportedLanguages : List [String ] = List (" scala" , " java" , " query " )
115116 def isSupportedLanguage (programmingLang : String ): Boolean =
116117 supportedLanguages.contains(APIUtil .normaliseDynamicCodeLanguage(programmingLang))
117- val supportedLanguagesText : String = " Scala, Java"
118+ val supportedLanguagesText : String = " Scala, Java, Query"
119+
120+ /**
121+ * True when the body is a Dynamic Query (`programming_lang` `Query`): a declaration, not code. It
122+ * runs no user code, so the switch for user-supplied code (`allow_user_generated_scala_code`) does
123+ * not apply to it, and it is read-only, so its doc's request_verb must be GET.
124+ */
125+ def isQuery (programmingLang : String ): Boolean = APIUtil .normaliseDynamicCodeLanguage(programmingLang) == " query"
126+
127+ /** A Dynamic Query only reads; every other language may use any verb. */
128+ def verbAllowed (programmingLang : String , requestVerb : String ): Boolean =
129+ ! isQuery(programmingLang) || requestVerb == " GET"
130+
131+ val queryVerbMessage : String = " A Dynamic Query only reads, so its request_verb must be GET."
118132
119133 /**
120134 * The native http4s template a method body is inlined into. Returns the full source and the
@@ -167,14 +181,26 @@ object CompiledObjects {
167181 * diagnostics with line numbers relative to the method body the author wrote, in the body's
168182 * programming language (Scala or Java). Empty = compiles.
169183 */
170- def compileProblems (exampleRequestBody : Option [JValue ], successResponseBody : Option [JValue ], methodBody : String , programmingLang : String = " Scala" ): List [DynamicUtil .CompileProblem ] = {
184+ def compileProblems (exampleRequestBody : Option [JValue ], successResponseBody : Option [JValue ], methodBody : String ,
185+ programmingLang : String = " Scala" , bankId : Option [String ] = None ): List [DynamicUtil .CompileProblem ] = {
171186 val decodedMethodBody = URLDecoder .decode(methodBody, " UTF-8" )
172187 // Java bodies are compiled as written (no template, no generated case classes), so the example
173- // bodies play no part, as they play none in CompiledObjects' own Java branch.
174- if (APIUtil .normaliseDynamicCodeLanguage(programmingLang) == " java" ) DynamicUtil .checkJavaCode(decodedMethodBody)
175- else scalaCompileProblems(exampleRequestBody, successResponseBody, decodedMethodBody)
188+ // bodies play no part, as they play none in CompiledObjects' own Java branch. A Dynamic Query is
189+ // not compiled at all: its problems are the declaration's, which have no line numbers.
190+ APIUtil .normaliseDynamicCodeLanguage(programmingLang) match {
191+ case " java" => DynamicUtil .checkJavaCode(decodedMethodBody)
192+ case " query" => queryProblem(decodedMethodBody, bankId).map(message => DynamicUtil .CompileProblem (0 , 0 , " ERROR" , message)).toList
193+ case _ => scalaCompileProblems(exampleRequestBody, successResponseBody, decodedMethodBody)
194+ }
176195 }
177196
197+ /** What is wrong with a Dynamic Query body, in full (error code included), or None when it is valid in `bankId`'s space. */
198+ def queryProblem (decodedMethodBody : String , bankId : Option [String ]): Option [String ] =
199+ code.api.dynamic.entity.query.DynamicQueryDeclaration .parse(decodedMethodBody) match {
200+ case Left (error) => Some (s " ${ErrorMessages .DynamicQueryInvalid }${error.message}" )
201+ case Right (declaration) => code.api.dynamic.entity.query.DynamicQuery .validate(bankId, declaration).left.toOption.map(_.message)
202+ }
203+
178204 private def scalaCompileProblems (exampleRequestBody : Option [JValue ], successResponseBody : Option [JValue ], decodedMethodBody : String ): List [DynamicUtil .CompileProblem ] = {
179205 val requestBody : Product = exampleRequestBody match {
180206 case Some (JString (s)) if StringUtils .isBlank(s) => toCaseObject(None )
@@ -205,17 +231,40 @@ object CompiledObjects {
205231 }
206232}
207233
208- case class CompiledObjects (exampleRequestBody : Option [JValue ], successResponseBody : Option [JValue ], methodBody : String , programmingLang : String = " Scala" ) {
234+ /**
235+ * `bankId` is the Dynamic Entity space of the doc (None for the system space). Only a Dynamic Query
236+ * uses it: its declaration names entities, which are looked up in that space.
237+ */
238+ case class CompiledObjects (exampleRequestBody : Option [JValue ], successResponseBody : Option [JValue ], methodBody : String ,
239+ programmingLang : String = " Scala" , bankId : Option [String ] = None ) {
209240 val decodedMethodBody = URLDecoder .decode(methodBody, " UTF-8" )
241+ private val isQuery = CompiledObjects .isQuery(programmingLang)
242+ // The stored doc of a system-level Dynamic Resource Doc carries Some(null) here, not None.
243+ private val space : Option [String ] = bankId.flatMap(Option (_)).map(_.trim).filter(_.nonEmpty)
244+
245+ // A Dynamic Query compiles nothing, not even case classes for its examples: toCaseObject generates
246+ // and compiles Scala, which is refused where user-supplied code is switched off. Its examples are
247+ // carried as JSON, which the resource-docs serialisation renders as they are.
248+ private def exampleOf (json : Option [JValue ]): Product =
249+ if (! isQuery) toCaseObject(json)
250+ else json.filter(j => j != JNothing && j != JNull ).map(code.api.berlin.group.v1_3.JvalueCaseClass (_)).getOrElse(EmptyBody )
251+
210252 val requestBody : Product = exampleRequestBody match {
211253 // this case means, we accept the empty string "" from json post body, we need to map it to None.
212- case Some (JString (s)) if StringUtils .isBlank(s) => toCaseObject (None )
254+ case Some (JString (s)) if StringUtils .isBlank(s) => exampleOf (None )
213255 // Here we will generate the object by the JValue (exampleRequestBody)
214- case _ => toCaseObject (exampleRequestBody)
256+ case _ => exampleOf (exampleRequestBody)
215257 }
216- val successResponse : Product = toCaseObject (successResponseBody)
258+ val successResponse : Product = exampleOf (successResponseBody)
217259
218260 private val partialFunction : Http4sEndpointIO = APIUtil .normaliseDynamicCodeLanguage(programmingLang) match {
261+ case " query" =>
262+ // A declaration, not code: parsed here (a malformed body cannot be served), checked against the
263+ // entity definitions by validateDependency, and run per request by DynamicQueryEndpoint.
264+ code.api.dynamic.entity.query.DynamicQueryDeclaration .parse(decodedMethodBody) match {
265+ case Right (declaration) => DynamicQueryEndpoint (declaration, space)
266+ case Left (error) => throw JsonResponseException (s " ${ErrorMessages .DynamicQueryInvalid }${error.message}" , 400 , " none" )
267+ }
219268 case " java" =>
220269 DynamicUtil .createJavaHttp4sEndpoint(decodedMethodBody) match {
221270 case Full (func) => func
@@ -286,6 +335,8 @@ case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBo
286335 */
287336 def validateDependency () = APIUtil .normaliseDynamicCodeLanguage(programmingLang) match {
288337 case " java" => ()
338+ // A Dynamic Query calls no methods; what it is checked against is the entity definitions of its space.
339+ case " query" => CompiledObjects .queryProblem(decodedMethodBody, space).foreach(message => throw JsonResponseException (message, 400 , " none" ))
289340 case _ => Validation .validateDependency(this .partialFunction)
290341 }
291342
@@ -294,7 +345,7 @@ case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBo
294345 * author wrote (the wrapper's own lines are subtracted). Empty = compiles. Nothing is evaluated or cached.
295346 */
296347 def compileProblems (): List [DynamicUtil .CompileProblem ] =
297- CompiledObjects .compileProblems(exampleRequestBody, successResponseBody, methodBody, programmingLang)
348+ CompiledObjects .compileProblems(exampleRequestBody, successResponseBody, methodBody, programmingLang, space )
298349
299350 /**
300351 * Wraps the compiled partial function as an endpoint. This used to bind a per-bank
0 commit comments