diff --git a/fury b/fury deleted file mode 100755 index 3583fee..0000000 --- a/fury +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -# -# This is the Fury launcher script, which will download and run Fury in "standalone" mode. You can -# distribute this file in a Git repository to make it easier for users to run Fury. -# -# Copyright 2018-20 Jon Pretty, Propensive OÜ. -# - -version="0.15.1" -ipfsHash="Qmaie8Mt3d72dsc6p8ysRYZz6fsr2rgvyWY7Y5JqQBbWfu" -xdgUsrHome="${XDG_DATA_HOME:-"$HOME/.local/share"}" -xdgSysHome="${XDG_DATA_HOME:-/usr/share}" -xdgHome="$([ "$EUID" = "0" ] && echo "${xdgSysHome}" || echo "${xdgUsrHome}")" -furyDir="${xdgHome}/fury" -furyUsr="${furyDir}/usr" -currentDir="${furyUsr}/current" -currentVersion="$(head -n1 "${currentDir}/.version" 2> /dev/null || echo "")" -downloadDir="${furyDir}/downloads" -downloadFile="${downloadDir}/fury-${version}.tar.gz" -ipfsGateway="https://gateway.pinata.cloud/ipfs" -downloadUrl="${ipfsGateway}/${ipfsHash}" -installDir="${furyUsr}/$([ "${currentVersion}" = "${version}" ] && echo "current" || echo "${version}")" -args="$@" - -installFury() { - mkdir -p "${downloadDir}" - printf "Downloading Fury ${version}..." - command -v curl > /dev/null || fail curl - [ -f "${downloadFile}" ] || curl -Lso "${downloadFile}" "${downloadUrl}" || fail download - printf "done\n" - printf "Extracting Fury ${version} into ${installDir}..." - command -v tar > /dev/null || fail tar - mkdir -p "${installDir}" && tar xf "${downloadFile}" -C "${installDir}" || fail extract - printf "done\n" -} - -runFury() { - printf "Launching Fury ${version} in standalone mode...\n" - FURY_HOME="${installDir}" "${installDir}/bin/fury" standalone ${args} -} - -fail() { - printf "fail\n" - case "$1" in - curl) printf "Could not find `curl` to download Fury\n" ;; - download) printf "Could not download Fury version ${version}\n" ;; - tar) printf "Could not find `tar` to extract Fury\n" ;; - extract) printf "Could not extract Fury\n" ;; - esac - exit 1 -} - -([ -d "${installDir}" ] || [ "${currentVersion}" = "${version}" ] || installFury) && runFury diff --git a/src/core/QueryBuilder.scala b/src/core/QueryBuilder.scala index 80bf531..7e09580 100644 --- a/src/core/QueryBuilder.scala +++ b/src/core/QueryBuilder.scala @@ -6,6 +6,7 @@ import language.experimental.macros import scala.collection.immutable.SortedMap import com.google.cloud.datastore.StructuredQuery.OrderBy.Direction import com.google.cloud.datastore.StructuredQuery.OrderBy +import com.google.cloud.datastore.{Query => DatastoreQuery} import quarantine._ case class QueryBuilder[T] private[mutatus] ( @@ -62,7 +63,7 @@ case class QueryBuilder[T] private[mutatus] ( decoder: Decoder[T] ): mutatus.Result[Stream[mutatus.Result[T]]] = { val baseQuery = namespace.option.foldLeft( - Query.newEntityQueryBuilder().setKind(kind) + DatastoreQuery.newEntityQueryBuilder().setKind(kind) )(_.setNamespace(_)) val filtered = filterCriteria.foldLeft(baseQuery)(_.setFilter(_)) val ordered = orderCriteria.headOption.foldLeft(filtered)( diff --git a/src/core/QueryBuilderMacros.scala b/src/core/QueryBuilderMacros.scala index ca30c77..5512484 100644 --- a/src/core/QueryBuilderMacros.scala +++ b/src/core/QueryBuilderMacros.scala @@ -25,19 +25,26 @@ class QueryBuilderMacros(val c: blackbox.Context) { q"_root_.com.google.cloud.datastore.StructuredQuery.PropertyFilter" private val composite = q"_root_.com.google.cloud.datastore.StructuredQuery.CompositeFilter.and" - private val operationMapping - : PartialFunction[c.Tree, (String, Option[c.Tree]) => c.Tree] = { - case q"==" | q"equals" => (path, args) => q"$filter.eq($path, ${args.get})" - case q"<" => (path, args) => q"$filter.lt($path, ${args.get})" - case q"<=" => (path, args) => q"$filter.le($path, ${args.get})" - case q">" => (path, args) => q"$filter.gt($path, ${args.get})" - case q">=" => (path, args) => q"$filter.ge($path, ${args.get})" + + def encodedArg(arg: Option[c.Tree]): c.Tree = { + val tpe = c.typecheck(arg.get).tpe + val tpeErasure = tpe.erasure + val usedTpe = if(tpeErasure != tpe && tpe.typeSymbol == tpeErasure.typeSymbol) tpe.erasure else tpe //Used to handle erasure of Int(1) => Int and also keeping in mind to not erase value classes + q"implicitly[_root_.mutatus.Encoder[$usedTpe]].encode(${arg.get})" + } + + private val operationMapping: PartialFunction[c.Tree, (String, Option[c.Tree]) => c.Tree] = { + case q"==" | q"equals" => (path, args) => q"$filter.eq($path, ${encodedArg(args)})" + case q"<" => (path, args) => q"$filter.lt($path, ${encodedArg(args)})" + case q"<=" => (path, args) => q"$filter.le($path, ${encodedArg(args)})" + case q">" => (path, args) => q"$filter.gt($path, ${encodedArg(args)})" + case q">=" => (path, args) => q"$filter.ge($path, ${encodedArg(args)})" case q"isEmpty" => (path, args) => q"$filter.isNull($path)" case q"isDefined" | q"nonEmpty" => (path, args) => q"$filter.gt($path, _root_.com.google.cloud.datastore.NullValue.of())" case q"contains" | q"foreach" => - (path, args) => q"$filter.eq($path, ${args.get})" + (path, args) => q"$filter.eq($path, ${encodedArg(args)})" } def filterImpl[T: c.WeakTypeTag]( diff --git a/src/core/mutatus.scala b/src/core/mutatus.scala index c0d3b57..3c721d3 100644 --- a/src/core/mutatus.scala +++ b/src/core/mutatus.scala @@ -24,7 +24,6 @@ import scala.annotation.StaticAnnotation import scala.collection.JavaConverters._ import scala.collection.generic.CanBuildFrom import scala.language.experimental.macros -import io.opencensus.trace.Status.CanonicalCode import com.google.rpc.Code /** Mutatus package object */ @@ -149,7 +148,7 @@ object `package` extends Domain[MutatusException] { } final class id() extends StaticAnnotation - +final class notIndexed() extends StaticAnnotation /** a reference to another case class instance stored in the GCP Datastore */ case class Ref[T](ref: Key) { @@ -331,7 +330,6 @@ object Decoder extends Decoder_1 { def combine[T](caseClass: CaseClass[Decoder, T]): Decoder[T] = { case entityValue: EntityValue => val entity = entityValue.get() - caseClass.constructMonadic { param => if (entity.contains(param.label)) { param.typeclass.decodeValue(entity.getValue(param.label)) @@ -347,7 +345,9 @@ object Decoder extends Decoder_1 { } } } - } + case value if caseClass.isValueClass=> + caseClass.constructMonadic{ param => param.typeclass.decodeValue(value) } + } /** tries to decode entity based on encoded `_meta.typename` value containing name of class, * if such value is missing tries `Decoder`s for each subtype of sealed trait `T` until one doesn't throw an exception @@ -479,21 +479,39 @@ object Encoder extends Encoder_1 { final val metaField = "_meta" final val typenameField = "typename" + private def excludeFromIndex(value: Value[_]): Value[_] = { + value match { + case coll: ListValue => ListValue.of { + coll.toBuilder.get().asScala.map(excludeFromIndex).asJava + } + case _ => value.toBuilder + .setExcludeFromIndexes(true).asInstanceOf[ValueBuilder[_,_,_]] + .build.asInstanceOf[Value[Any]] + } + } + /** combines `Encoder`s for each parameter of the case class `T` into a `Encoder` for `T` */ def combine[T](caseClass: CaseClass[Encoder, T]): Encoder[T] = value => - EntityValue.of { + if(caseClass.isValueClass) { + val param = caseClass.parameters.head + param.typeclass.encode(param.dereference(value)).asInstanceOf[Value[Any]] + } else EntityValue.of { caseClass.parameters - .to[List] + .toList .foldLeft(FullEntity.newBuilder()) { case (b, param) => + val encodedValue = param.typeclass.encode(param.dereference(value)) + val isNotIndexed = param.annotations.find(_.isInstanceOf[notIndexed]) b.set( param.label, - param.typeclass.encode(param.dereference(value)) + isNotIndexed.foldLeft[Value[_]](encodedValue) { + case (value, _) => excludeFromIndex(value) + } ) } .build() - } + } /** chooses the appropriate `Encoder` of a subtype of the sealed trait `T` based on its type */ def dispatch[T](sealedTrait: SealedTrait[Encoder, T]): Encoder[T] = diff --git a/src/tests/QueryBuilderSpec.scala b/src/tests/QueryBuilderSpec.scala index 3733ed2..eb199dd 100644 --- a/src/tests/QueryBuilderSpec.scala +++ b/src/tests/QueryBuilderSpec.scala @@ -237,17 +237,28 @@ case class QueryBuilderSpec()(implicit runner: Runner) { .reverse .sortBy(_.intParam) }.assert { query => - query.orderCriteria.foreach(println) query.orderCriteria.toList == List( OrderBy.asc("intParam"), OrderBy.desc("innerClass.deeperOpt.optInner.some3"), OrderBy.asc("innerClassOpt.deeper.some2") ) } + + test("build filter with value classes") { + val id = EntityId(2) + Dao[Entity].all + .filter(_.id == id) + }.assert { _.filterCriteria.contains{ + PropertyFilter.eq("id", LongValue.of(2)) + } +} } object QueryBuilderSpec { object Model { + case class EntityId(id: Long) extends AnyVal + case class Entity(id: EntityId, value: String) + case class InnerClass3(some3: Int = 3, none: Option[String] = None) case class InnerClass2( some2: Int = 2,