From 3804b85e6af5cd936d25dec773d9b05df48336fc Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Tue, 16 Feb 2016 10:44:59 -0500 Subject: [PATCH 01/12] Split project into rtm-scala and rtm-scala-meta --- build.gradle | 96 +++++-------------- gradle/publishing.gradle | 64 +++++++++++++ .../com/github/vergenzt/rtmscala/domain.scala | 0 .../com/github/vergenzt/rtmscala/rtm.scala | 0 .../rtmscala/util/ParamConversions.scala | 0 .../rtmscala/util/XmlConversions.scala | 0 .../vergenzt/rtmscala/util/package.scala | 0 .../github/vergenzt/rtmscala/RtmTest.scala | 0 .../rtmscala/XmlConversionsTest.scala | 0 settings.gradle | 3 + 10 files changed, 89 insertions(+), 74 deletions(-) create mode 100644 gradle/publishing.gradle rename {src => rtm-scala/src}/main/scala/com/github/vergenzt/rtmscala/domain.scala (100%) rename {src => rtm-scala/src}/main/scala/com/github/vergenzt/rtmscala/rtm.scala (100%) rename {src => rtm-scala/src}/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala (100%) rename {src => rtm-scala/src}/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala (100%) rename {src => rtm-scala/src}/main/scala/com/github/vergenzt/rtmscala/util/package.scala (100%) rename {src => rtm-scala/src}/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala (100%) rename {src => rtm-scala/src}/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala (100%) diff --git a/build.gradle b/build.gradle index 604b9de..61387c9 100644 --- a/build.gradle +++ b/build.gradle @@ -2,92 +2,40 @@ plugins { id "com.github.maiflai.scalatest" version "0.6-5-g9065d91" } -apply plugin: "scala" -apply plugin: "eclipse" -apply plugin: "maven" -apply plugin: "signing" - -apply from: "gradle/credentials.gradle" - group = "com.github.vergenzt" version = "0.1.4" -repositories { - mavenCentral() -} - -dependencies { - compile "org.scala-lang:scala-library:2.11.5" +subprojects { + apply plugin: "scala" + apply plugin: "eclipse" - compile "org.scala-lang.modules:scala-xml_2.11:1.0.3" - compile "org.scalaj:scalaj-http_2.11:1.1.4" - compile "com.github.nscala-time:nscala-time_2.11:1.8.0" + repositories { + mavenCentral() + } - testCompile "org.scalatest:scalatest_2.11:2.2.4" - testCompile "org.mockito:mockito-core:1.10.19" - testCompile "com.squareup.okhttp:mockwebserver:2.3.0" + dependencies { + compile "org.scala-lang:scala-library:2.11.5" - // for scalatest gradle plugin - testRuntime "org.pegdown:pegdown:1.1.0" -} + compile "org.scala-lang.modules:scala-xml_2.11:1.0.3" + compile "org.scalaj:scalaj-http_2.11:1.1.4" + compile "com.github.nscala-time:nscala-time_2.11:1.8.0" -task sourcesJar(type: Jar) { - classifier = "sources" - from sourceSets.main.allSource -} + testCompile "org.scalatest:scalatest_2.11:2.2.4" + testCompile "org.mockito:mockito-core:1.10.19" + testCompile "com.squareup.okhttp:mockwebserver:2.3.0" -task scaladocJar(type: Jar, dependsOn: scaladoc) { - classifier = "javadoc" - from scaladoc.destinationDir -} - -artifacts { - archives jar - archives sourcesJar - archives scaladocJar + // for scalatest gradle plugin + testRuntime "org.pegdown:pegdown:1.1.0" + } } -signing { - sign configurations.archives +project(":rtm-scala") { + apply from: "$rootProject.rootDir/gradle/publishing.gradle" } -uploadArchives { - repositories.mavenDeployer { - beforeDeployment { deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: "", password: "") // set in credentials.gradle - } - - pom.project { - name "rtm-scala_2.11" - packaging "jar" - description "rtm-scala is a Scala wrapper for the Remember the Milk API." - url "https://github.com/vergenzt/rtm-scala" - - scm { - url "scm:git@github.com:vergenzt/rtm-scala.git" - connection "scm:git@github.com:vergenzt/rtm-scala.git" - developerConnection "scm:git@github.com:vergenzt/rtm-scala.git" - } - - licenses { - license { - name "MIT License" - url "http://opensource.org/licenses/MIT" - distribution "repo" - } - } - - developers { - developer { - id "vergenzt" - name "Tim Vergenz" - email "vergenzt@gmail.com" - url "https://github.com/vergenzt" - } - } - } +project(":rtm-scala-meta") { + dependencies { + compile project(":rtm-scala") } } diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle new file mode 100644 index 0000000..4c9f483 --- /dev/null +++ b/gradle/publishing.gradle @@ -0,0 +1,64 @@ +apply plugin: "maven" +apply plugin: "signing" +apply from: "$rootProject.rootDir/gradle/credentials.gradle" + +task sourcesJar(type: Jar) { + classifier = "sources" + from sourceSets.main.allSource +} + +task scaladocJar(type: Jar, dependsOn: scaladoc) { + classifier = "javadoc" + from scaladoc.destinationDir +} + +artifacts { + archives jar + archives sourcesJar + archives scaladocJar +} + +signing { + sign configurations.archives +} + +uploadArchives { + repositories.mavenDeployer { + beforeDeployment { deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: "", password: "") // set in credentials.gradle + } + + pom.project { + name "rtm-scala_2.11" + packaging "jar" + description "rtm-scala is a Scala wrapper for the Remember the Milk API." + url "https://github.com/vergenzt/rtm-scala" + + scm { + url "scm:git@github.com:vergenzt/rtm-scala.git" + connection "scm:git@github.com:vergenzt/rtm-scala.git" + developerConnection "scm:git@github.com:vergenzt/rtm-scala.git" + } + + licenses { + license { + name "MIT License" + url "http://opensource.org/licenses/MIT" + distribution "repo" + } + } + + developers { + developer { + id "vergenzt" + name "Tim Vergenz" + email "vergenzt@gmail.com" + url "https://github.com/vergenzt" + } + } + } + } +} + diff --git a/src/main/scala/com/github/vergenzt/rtmscala/domain.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/domain.scala similarity index 100% rename from src/main/scala/com/github/vergenzt/rtmscala/domain.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/domain.scala diff --git a/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala similarity index 100% rename from src/main/scala/com/github/vergenzt/rtmscala/rtm.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala diff --git a/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala similarity index 100% rename from src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala diff --git a/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala similarity index 100% rename from src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala diff --git a/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala similarity index 100% rename from src/main/scala/com/github/vergenzt/rtmscala/util/package.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala diff --git a/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala similarity index 100% rename from src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala rename to rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala diff --git a/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala similarity index 100% rename from src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala rename to rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala diff --git a/settings.gradle b/settings.gradle index 7374dd6..4c55d43 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ +include 'rtm-scala' +include 'rtm-scala-meta' + rootProject.name = "rtm-scala_2.11" From 01ee9ffb18be68384a1caece8a23dd4b19bb3bca Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Tue, 16 Feb 2016 13:08:04 -0500 Subject: [PATCH 02/12] First working version of RtmApiGenerated Also splits rtm-scala into rtm-scala and rtm-scala-lib. --- build.gradle | 22 ++++++++++++++-- .../com/github/vergenzt/rtmscala/domain.scala | 0 .../rtmscala/util/ParamConversions.scala | 0 .../rtmscala/util/XmlConversions.scala | 0 .../vergenzt/rtmscala/util/package.scala | 0 .../rtmscala/XmlConversionsTest.scala | 0 .../rtmscala/meta/generateRtmApi.scala | 25 +++++++++++++++++++ .../com/github/vergenzt/rtmscala/rtm.scala | 5 +--- .../vergenzt/rtmscala/rtm_generated.scala | 5 ++++ .../github/vergenzt/rtmscala/RtmTest.scala | 15 +++++------ settings.gradle | 1 + 11 files changed, 58 insertions(+), 15 deletions(-) rename {rtm-scala => rtm-scala-lib}/src/main/scala/com/github/vergenzt/rtmscala/domain.scala (100%) rename {rtm-scala => rtm-scala-lib}/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala (100%) rename {rtm-scala => rtm-scala-lib}/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala (100%) rename {rtm-scala => rtm-scala-lib}/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala (100%) rename {rtm-scala => rtm-scala-lib}/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala (100%) create mode 100644 rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala create mode 100644 rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala diff --git a/build.gradle b/build.gradle index 61387c9..f3a46cc 100644 --- a/build.gradle +++ b/build.gradle @@ -13,13 +13,20 @@ subprojects { mavenCentral() } + configurations { + scalaCompilerPlugin + } + dependencies { - compile "org.scala-lang:scala-library:2.11.5" + compile "org.scala-lang:scala-library:2.11.7" + compile "org.scala-lang:scala-compiler:2.11.7" compile "org.scala-lang.modules:scala-xml_2.11:1.0.3" compile "org.scalaj:scalaj-http_2.11:1.1.4" compile "com.github.nscala-time:nscala-time_2.11:1.8.0" + scalaCompilerPlugin "org.scalamacros:paradise_2.11.7:2.1.0" + testCompile "org.scalatest:scalatest_2.11:2.2.4" testCompile "org.mockito:mockito-core:1.10.19" testCompile "com.squareup.okhttp:mockwebserver:2.3.0" @@ -27,15 +34,26 @@ subprojects { // for scalatest gradle plugin testRuntime "org.pegdown:pegdown:1.1.0" } + + tasks.withType(ScalaCompile) { + scalaCompileOptions.additionalParameters = [ + "-Xplugin:" + configurations.scalaCompilerPlugin.asPath + ] + } } project(":rtm-scala") { apply from: "$rootProject.rootDir/gradle/publishing.gradle" + + dependencies { + compile project(":rtm-scala-lib") + compile project(":rtm-scala-meta") + } } project(":rtm-scala-meta") { dependencies { - compile project(":rtm-scala") + compile project(":rtm-scala-lib") } } diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/domain.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala similarity index 100% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/domain.scala rename to rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala similarity index 100% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala rename to rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/ParamConversions.scala diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala similarity index 100% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala rename to rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala similarity index 100% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala rename to rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/package.scala diff --git a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala b/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala similarity index 100% rename from rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala rename to rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala new file mode 100644 index 0000000..22106e6 --- /dev/null +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala @@ -0,0 +1,25 @@ +package com.github.vergenzt.rtmscala.meta + +import scala.annotation.StaticAnnotation + +import scala.language.experimental.macros +import scala.reflect.macros.blackbox.Context + +class generateRtmApi extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro GenerateRtmApiImpl.generateRtmApiImpl +} + +object GenerateRtmApiImpl { + def generateRtmApiImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + annottees.map(_.tree) match { + case List(q"trait RtmApiGenerated") => + + c.Expr[Any](q""" + trait RtmApiGenerated { + def testMethod() = println("Yo yo yo") + } + """) + } + } +} diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala index 3334fe6..84618f0 100644 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala @@ -2,12 +2,9 @@ package com.github.vergenzt.rtmscala import scala.concurrent.ExecutionContext import scala.concurrent.Future -import scala.xml.NodeSeq - import org.joda.time.DateTime import org.joda.time.LocalDate import org.joda.time.LocalDateTime - import scalaj.http.Http import scalaj.http.HttpRequest import util._ @@ -26,7 +23,7 @@ import util.XmlConversions._ * @define perms @param perms The requested permission. * @define timeline @param timeline A timeline obtained from `rtm.timelines.create` */ -object rtm { +object rtm extends RtmApiGenerated { val BASE_URL = "http://api.rememberthemilk.com/services" val AUTH_URL = BASE_URL + "/auth/" diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala new file mode 100644 index 0000000..72d8f62 --- /dev/null +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala @@ -0,0 +1,5 @@ +package com.github.vergenzt.rtmscala + +import com.github.vergenzt.rtmscala.meta._ + +@generateRtmApi trait RtmApiGenerated diff --git a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala index bde4d3b..578e9bb 100644 --- a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala +++ b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala @@ -2,22 +2,13 @@ package com.github.vergenzt.rtmscala import java.util.logging.Level import java.util.logging.Logger - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future - -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.spy -import org.mockito.Mockito.when import org.scalatest.BeforeAndAfter import org.scalatest.FunSpec import org.scalatest.concurrent.ScalaFutures import org.scalatest.mock.MockitoSugar - import com.squareup.okhttp.mockwebserver.MockResponse import com.squareup.okhttp.mockwebserver.MockWebServer import com.squareup.okhttp.mockwebserver.RecordedRequest - import scalaj.http.HttpConstants class RtmTest extends FunSpec with BeforeAndAfter with MockitoSugar with ScalaFutures { @@ -59,6 +50,12 @@ class RtmTest extends FunSpec with BeforeAndAfter with MockitoSugar with ScalaFu implicit val authToken = AuthToken("314159", Permission.Delete, User("1", "bob", Some("Bob T. Monkey"))) implicit val timeline = Timeline("54321") + describe("testMethod") { + it("works") { + rtm.testMethod() + } + } + describe("auth") { it("signs correctly") { diff --git a/settings.gradle b/settings.gradle index 4c55d43..ee856e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include 'rtm-scala' +include 'rtm-scala-lib' include 'rtm-scala-meta' rootProject.name = "rtm-scala_2.11" From fff7a99c34b602b8618c14d44f494b2c9ef6b8c2 Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Tue, 16 Feb 2016 13:13:27 -0500 Subject: [PATCH 03/12] Rename rtm => RtmApi with alias --- .../github/vergenzt/rtmscala/{rtm.scala => RtmApi.scala} | 2 +- .../{rtm_generated.scala => RtmApiGenerated.scala} | 0 .../main/scala/com/github/vergenzt/rtmscala/package.scala | 8 ++++++++ .../vergenzt/rtmscala/{RtmTest.scala => RtmApiTest.scala} | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) rename rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/{rtm.scala => RtmApi.scala} (99%) rename rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/{rtm_generated.scala => RtmApiGenerated.scala} (100%) create mode 100644 rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/package.scala rename rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/{RtmTest.scala => RtmApiTest.scala} (97%) diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala similarity index 99% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala index 84618f0..e93f679 100644 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm.scala +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala @@ -23,7 +23,7 @@ import util.XmlConversions._ * @define perms @param perms The requested permission. * @define timeline @param timeline A timeline obtained from `rtm.timelines.create` */ -object rtm extends RtmApiGenerated { +object RtmApi extends RtmApiGenerated { val BASE_URL = "http://api.rememberthemilk.com/services" val AUTH_URL = BASE_URL + "/auth/" diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala similarity index 100% rename from rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/rtm_generated.scala rename to rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/package.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/package.scala new file mode 100644 index 0000000..37cddbe --- /dev/null +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/package.scala @@ -0,0 +1,8 @@ +package com.github.vergenzt + +package object rtmscala { + + /** Alias to RtmApi to make calls shorter. **/ + val rtm = RtmApi + +} diff --git a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala similarity index 97% rename from rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala rename to rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala index 578e9bb..fb9cdb9 100644 --- a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmTest.scala +++ b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala @@ -11,7 +11,7 @@ import com.squareup.okhttp.mockwebserver.MockWebServer import com.squareup.okhttp.mockwebserver.RecordedRequest import scalaj.http.HttpConstants -class RtmTest extends FunSpec with BeforeAndAfter with MockitoSugar with ScalaFutures { +class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with ScalaFutures { var server: MockWebServer = _ From cb7aa1f362340a07f90075317250ef740759cbd1 Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Tue, 16 Feb 2016 14:04:15 -0500 Subject: [PATCH 04/12] Create domain objects and XML converters for reflection data --- .../com/github/vergenzt/rtmscala/domain.scala | 28 +++++++++ .../rtmscala/util/XmlConversions.scala | 33 ++++++++++ .../rtmscala/XmlConversionsTest.scala | 60 +++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala index 94bfcad..a1e3664 100644 --- a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala @@ -23,6 +23,9 @@ object Permission { case object Read extends Permission("read", 1) case object Write extends Permission("write", 2) case object Delete extends Permission("delete", 3) + + val ALL = Seq(None, Read, Write, Delete) + def fromInt(value: Int) = ALL.find(_.value == value) } case class User(id: String, username: String, fullname: Option[String]) @@ -118,6 +121,31 @@ case class Transaction( request: HttpRequest ) +/* Reflection */ + +case class MethodDesc( + name: String, + needsLogin: Boolean, + needsSigning: Boolean, + requiredPerms: Permission, + description: String, + exampleResponse: String, + arguments: Seq[ArgumentDesc], + errors: Seq[ErrorDesc] +) + +case class ArgumentDesc( + name: String, + optional: Boolean, + description: String +) + +case class ErrorDesc( + code: Int, + message: String, + description: String +) + /* Exceptions */ case class RtmException(val message: String, val code: Int) extends Exception { diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala index beb6375..9dbc693 100644 --- a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala @@ -160,4 +160,37 @@ object XmlConversions { implicit def xml2Timeline(xml: NodeSeq): Timeline = xml match { case Seq(Node("timeline", _, id)) => Timeline(id.text) } + + /* Reflection */ + + implicit def xml2MethodList(xml: NodeSeq): Seq[String] = { + for { + method <- xml \\ "method" + } yield method.text + } + + implicit def xml2MethodDesc(xml: NodeSeq): MethodDesc = xml match { + case Seq(method @ Node("method", _, body @ _*)) => MethodDesc( + name = method \@ "name", + needsLogin = method \@ "needslogin", + needsSigning = method \@ "needssigning", + requiredPerms = Permission.fromInt((method \@ "requiredperms").toInt).get, + description = (method \ "description").text, + exampleResponse = (method \ "response").text, + arguments = (method \\ "argument") map { arg => + ArgumentDesc( + name = arg \@ "name", + optional = arg \@ "optional", + description = arg.text + ) + }, + errors = (method \\ "error") map { err => + ErrorDesc( + code = (err \@ "code").toInt, + message = err \@ "message", + description = err.text + ) + } + ) + } } diff --git a/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala b/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala index ce175d6..147eb58 100644 --- a/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala +++ b/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala @@ -227,4 +227,64 @@ class XmlConversionsTest extends FlatSpec with Matchers { xml2Timeline("""12741021""") should be (Timeline("12741021")) } + + /* Reflection */ + + "xml2MethodList" should "work" in { + xml2MethodList(""" + + rtm.tasks.add + rtm.tasks.addTags + rtm.tasks.complete + rtm.tasks.delete + + """) should be ( + Seq("rtm.tasks.add", "rtm.tasks.addTags", "rtm.tasks.complete", "rtm.tasks.delete") + ) + } + + "xml2MethodDesc" should "work" in { + xml2MethodDesc(""" + + A testing method which checks if the caller is logged in. + <user id="987654321"> + <username>bob<username> + </user> + + + Your API application key. + <a href="/services/api/api_key.rtm">See here</a> for more details. + ... + + + The passed signature was invalid. + ... + + + """) should be ( + MethodDesc( + name = "rtm.test.login", + needsLogin = true, + needsSigning = true, + requiredPerms = Permission.Read, + description = "A testing method which checks if the caller is logged in.", + exampleResponse = """ bob """.trim, + arguments = Seq( + ArgumentDesc( + name = "api_key", + optional = false, + description = """Your API application key. See here for more details.""" + ) + ), + errors = Seq( + ErrorDesc( + code = 96, + message = "Invalid signature", + description = "The passed signature was invalid." + ) + ) + ) + ) + } + } From adfc777432ef6525e349cedd1a53bf561ed563dd Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Tue, 16 Feb 2016 14:06:48 -0500 Subject: [PATCH 05/12] Split base functionality into separate RtmApiBase --- .../github/vergenzt/rtmscala/RtmApiBase.scala | 91 +++++++++++++++++++ .../com/github/vergenzt/rtmscala/RtmApi.scala | 75 +-------------- 2 files changed, 92 insertions(+), 74 deletions(-) create mode 100644 rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala new file mode 100644 index 0000000..72c2259 --- /dev/null +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala @@ -0,0 +1,91 @@ +package com.github.vergenzt.rtmscala + +import scalaj.http.Http +import scalaj.http.HttpRequest +import util._ +import util.ParamConversions._ +import util.XmlConversions._ + +object RtmApiBase extends RtmApiBase + +/** + * Base trait for the Remember the Milk API. + * + * @define authToken @param authToken An authentication token. + * @define creds @param creds Your API credentials. + * @define method @param method The API method to call, without the prefix "rtm." + * (e.g."rtm.auth.getFrob" would be passed as "auth.getFrob") + * @define params @param params The method-specific parameters. + * @define timeline @param timeline A timeline obtained from `rtm.timelines.create` + */ +trait RtmApiBase { + + val BASE_URL = "http://api.rememberthemilk.com/services" + val AUTH_URL = BASE_URL + "/auth/" + + // for testing + private[rtmscala] var REST_URL = BASE_URL + "/rest/" + + /** + * Construct an RTM API request. + * @param url The url to request. + * $params + * $creds + */ + protected[rtmscala] + def baseRequest(url: String, params: (String, String)*) + (implicit creds: ApiCreds): HttpRequest = { + Http(url).param("api_key", creds.apiKey).params(params) + } + + /** + * Construct an unsigned RTM API request. + * $method + * $params + * $creds + */ + protected[rtmscala] + def unsignedRequest(method: String, params: (String, String)*) + (implicit creds: ApiCreds): HttpRequest = { + baseRequest(REST_URL, ("method" -> ("rtm." + method)) :: params.toList: _*) + } + + /** + * Construct a signed RTM API request. + * $method + * $params + * $creds + */ + protected[rtmscala] + def request(method: String, params: (String, String)*) + (implicit creds: ApiCreds): HttpRequest = { + unsignedRequest(method, params: _*).signed + } + + /** + * Construct an authenticated RTM API request. + * $method + * $params + * $creds + * $authToken + */ + protected[rtmscala] + def authedRequest(method: String, params: (String, String)*) + (implicit creds: ApiCreds, authToken: AuthToken) = { + request(method, ("auth_token" -> authToken.token) :: params.toList: _*) + } + + /** + * Construct an authenticated RTM API request with a timeline. + * $method + * $params + * $creds + * $authToken + * $timeline + */ + protected[rtmscala] + def timelinedRequest(method: String, params: (String, String)*) + (implicit creds: ApiCreds, authToken: AuthToken, timeline: Timeline) = { + authedRequest(method, ("timeline" -> timeline.id) :: params.toList: _*) + } +} diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala index e93f679..b6294f6 100644 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala @@ -23,80 +23,7 @@ import util.XmlConversions._ * @define perms @param perms The requested permission. * @define timeline @param timeline A timeline obtained from `rtm.timelines.create` */ -object RtmApi extends RtmApiGenerated { - - val BASE_URL = "http://api.rememberthemilk.com/services" - val AUTH_URL = BASE_URL + "/auth/" - - // for testing - private[rtmscala] var REST_URL = BASE_URL + "/rest/" - - /** - * Construct an RTM API request. - * @param url The url to request. - * $params - * $creds - */ - protected[rtmscala] - def baseRequest(url: String, params: (String, String)*) - (implicit creds: ApiCreds): HttpRequest = { - Http(url).param("api_key", creds.apiKey).params(params) - } - - /** - * Construct an unsigned RTM API request. - * $method - * $params - * $creds - */ - protected[rtmscala] - def unsignedRequest(method: String, params: (String, String)*) - (implicit creds: ApiCreds): HttpRequest = { - baseRequest(REST_URL, ("method" -> ("rtm." + method)) :: params.toList: _*) - } - - /** - * Construct a signed RTM API request. - * $method - * $params - * $creds - */ - protected[rtmscala] - def request(method: String, params: (String, String)*) - (implicit creds: ApiCreds): HttpRequest = { - unsignedRequest(method, params: _*).signed - } - - /** - * Construct an authenticated RTM API request. - * $method - * $params - * $creds - * $authToken - */ - protected[rtmscala] - def authedRequest(method: String, params: (String, String)*) - (implicit creds: ApiCreds, authToken: AuthToken) = { - request(method, ("auth_token" -> authToken.token) :: params.toList: _*) - } - - /** - * Construct an authenticated RTM API request with a timeline. - * $method - * $params - * $creds - * $authToken - * $timeline - */ - protected[rtmscala] - def timelinedRequest(method: String, params: (String, String)*) - (implicit creds: ApiCreds, authToken: AuthToken, timeline: Timeline) = { - authedRequest(method, ("timeline" -> timeline.id) :: params.toList: _*) - } - - /***************************** - * Begin API implementation. * - *****************************/ +object RtmApi extends RtmApiBase with RtmApiGenerated { object auth { /** From a69e54aed8465b6563fbaacf7a2ca532ce37dfa4 Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Wed, 17 Feb 2016 08:03:37 -0800 Subject: [PATCH 06/12] Apply annotation directly to RtmApi; develop macro --- .../github/vergenzt/rtmscala/RtmApiBase.scala | 13 ++++ .../rtmscala/meta/generateRtmApi.scala | 70 +++++++++++++++++-- .../com/github/vergenzt/rtmscala/RtmApi.scala | 10 ++- .../vergenzt/rtmscala/RtmApiGenerated.scala | 5 -- .../github/vergenzt/rtmscala/RtmApiTest.scala | 4 +- 5 files changed, 83 insertions(+), 19 deletions(-) delete mode 100644 rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala index 72c2259..e301178 100644 --- a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/RtmApiBase.scala @@ -88,4 +88,17 @@ trait RtmApiBase { (implicit creds: ApiCreds, authToken: AuthToken, timeline: Timeline) = { authedRequest(method, ("timeline" -> timeline.id) :: params.toList: _*) } + + /* Reflection methods */ + + object reflection { + def getMethodNames()(implicit creds: ApiCreds) = + request("reflection.getMethods").as[Seq[String]] + + def getMethod(methodName: String)(implicit creds: ApiCreds) = + request("reflection.getMethodInfo", "method_name" -> methodName).as[MethodDesc] + + def getMethods()(implicit creds: ApiCreds) = + getMethodNames().map(getMethod) + } } diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala index 22106e6..e304caf 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala @@ -1,25 +1,83 @@ package com.github.vergenzt.rtmscala.meta import scala.annotation.StaticAnnotation - import scala.language.experimental.macros -import scala.reflect.macros.blackbox.Context +import scala.reflect.macros.whitebox.Context +import java.net._ +import com.github.vergenzt.rtmscala._ +import com.github.vergenzt.rtmscala.util._ +import com.github.vergenzt.rtmscala.util.XmlConversions._ +import scala.annotation.compileTimeOnly -class generateRtmApi extends StaticAnnotation { +@compileTimeOnly("This is a macro requiring macro paradise.") +class GenerateRtmApi extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro GenerateRtmApiImpl.generateRtmApiImpl } object GenerateRtmApiImpl { + implicit val creds = ApiCreds(System.getProperty("rtm.api_key"), System.getProperty("rtm.api_secret")) + + /* Useful utility methods */ + private implicit class MethodDescExtra(method: MethodDesc) { + def nameParts: Array[String] = method.name.split('.').drop(1) + def group: String = nameParts.head + def methodName: String = nameParts.tail.mkString(".") + } + + /** Generate an implementation for RtmApi. */ def generateRtmApiImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ + setUpRtmCaching() + annottees.map(_.tree) match { - case List(q"trait RtmApiGenerated") => + case scala.List(q"object $rtm extends $rtmBase { ..$rtmCustomImpl }") => + + val methods = RtmApiBase.reflection.getMethods().filter(_.group != "reflection") + + // make an object for each group and an implementation for each method + val groupImpls = methods.groupBy(_.group) + .map({ case (group, methods) => + val methodImpls = methods.map(method => { + q"""def ${TermName(method.methodName)}() = println(${method.methodName})""" + }) + + q""" + object ${TermName(group)} { + ..$methodImpls + } + """ + }) + + // TODO merge in the custom methods c.Expr[Any](q""" - trait RtmApiGenerated { - def testMethod() = println("Yo yo yo") + object $rtm extends $rtmBase { + ..$groupImpls } """) } } + + /** Set up caching of RTM reflection api responses to decrease build times */ + def setUpRtmCaching() = { + import java.net._ + import java.util._ + import java.io._ + ResponseCache.setDefault(new ResponseCache() { + val cacheDir = "build/rtm-cache" + new File(cacheDir).mkdirs() + def get(uri: URI, method: String, headers: Map[String, List[String]]) = new CacheResponse { + val reader = new FileInputStream(cacheDir + "/" + uri.getQuery()) + val headers = new ObjectInputStream(reader).readObject().asInstanceOf[Map[String,List[String]]] + def getBody(): InputStream = reader + def getHeaders(): Map[String,List[String]] = headers + } + def put(uri: URI, conn: URLConnection) = new CacheRequest { + val writer = new FileOutputStream(cacheDir + "/" + uri.getQuery(), true) + new ObjectOutputStream(writer).writeObject(conn.getHeaderFields) + def abort(): Unit = writer.close() + def getBody(): OutputStream = writer + } + }) + } } diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala index b6294f6..cea7893 100644 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala @@ -7,9 +7,11 @@ import org.joda.time.LocalDate import org.joda.time.LocalDateTime import scalaj.http.Http import scalaj.http.HttpRequest + import util._ import util.ParamConversions._ import util.XmlConversions._ +import meta._ /** * Scala wrapper for the Remember the Milk API. @@ -23,7 +25,8 @@ import util.XmlConversions._ * @define perms @param perms The requested permission. * @define timeline @param timeline A timeline obtained from `rtm.timelines.create` */ -object RtmApi extends RtmApiBase with RtmApiGenerated { +@GenerateRtmApi +object RtmApi extends RtmApiBase { object auth { /** @@ -134,11 +137,6 @@ object RtmApi extends RtmApiBase with RtmApiGenerated { authedRequest("locations.getList").as[Seq[Location]] } - object reflection { - // TODO: getMethodInfo - // TODO: getMethods - } - object settings { // TODO: getList } diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala deleted file mode 100644 index 72d8f62..0000000 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApiGenerated.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.vergenzt.rtmscala - -import com.github.vergenzt.rtmscala.meta._ - -@generateRtmApi trait RtmApiGenerated diff --git a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala index fb9cdb9..cd0c57c 100644 --- a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala +++ b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala @@ -50,9 +50,9 @@ class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with Scal implicit val authToken = AuthToken("314159", Permission.Delete, User("1", "bob", Some("Bob T. Monkey"))) implicit val timeline = Timeline("54321") - describe("testMethod") { + describe("test_dynamic") { it("works") { - rtm.testMethod() + rtm.test } } From e2c0ee4bfe21006d15b09acdcf1236153a16b32f Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Wed, 17 Feb 2016 08:03:54 -0800 Subject: [PATCH 07/12] Attempt to use Eclipse to debug macro execution --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f3a46cc..c9a3426 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,10 @@ subprojects { tasks.withType(ScalaCompile) { scalaCompileOptions.additionalParameters = [ - "-Xplugin:" + configurations.scalaCompilerPlugin.asPath + "-Xplugin:" + configurations.scalaCompilerPlugin.asPath, + "-Ymacro-debug-lite" ] + options.compilerArgs = ["-Xdebug", "-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=9999"] } } From 1d8841958d18fbe1720d011d99432c35064b5d45 Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Wed, 17 Feb 2016 21:20:46 -0800 Subject: [PATCH 08/12] Fix rtm api caching --- .../rtmscala/meta/generateRtmApi.scala | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala index e304caf..fbdc4ec 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala @@ -64,20 +64,30 @@ object GenerateRtmApiImpl { import java.util._ import java.io._ ResponseCache.setDefault(new ResponseCache() { - val cacheDir = "build/rtm-cache" + val cacheDir = System.getProperty("user.home") + "/.rtm-scala-meta/build-cache" new File(cacheDir).mkdirs() - def get(uri: URI, method: String, headers: Map[String, List[String]]) = new CacheResponse { - val reader = new FileInputStream(cacheDir + "/" + uri.getQuery()) - val headers = new ObjectInputStream(reader).readObject().asInstanceOf[Map[String,List[String]]] - def getBody(): InputStream = reader - def getHeaders(): Map[String,List[String]] = headers - } - def put(uri: URI, conn: URLConnection) = new CacheRequest { - val writer = new FileOutputStream(cacheDir + "/" + uri.getQuery(), true) - new ObjectOutputStream(writer).writeObject(conn.getHeaderFields) - def abort(): Unit = writer.close() - def getBody(): OutputStream = writer - } + def get(uri: URI, method: String, headers: Map[String, List[String]]) = + try { + val reader = new FileInputStream(cacheDir + "/" + uri.getQuery()) + val headers = new ObjectInputStream(reader).readObject().asInstanceOf[Map[String,List[String]]] + new CacheResponse { + def getBody(): InputStream = reader + def getHeaders(): Map[String,List[String]] = headers + } + } catch { + case _: IOException => null + } + def put(uri: URI, conn: URLConnection) = + try { + val writer = new FileOutputStream(cacheDir + "/" + uri.getQuery(), true) + new ObjectOutputStream(writer).writeObject(conn.getHeaderFields) + new CacheRequest { + def abort(): Unit = writer.close() + def getBody(): OutputStream = writer + } + } catch { + case _: IOException => null + } }) } } From 03dc662e98bb9dc71b77ef8809a9a8037194c0ec Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Wed, 17 Feb 2016 23:52:43 -0800 Subject: [PATCH 09/12] Split rtm api caching into separate file --- .../rtmscala/meta/generateRtmApi.scala | 44 +++---------------- .../vergenzt/rtmscala/util/HttpCaching.scala | 37 ++++++++++++++++ 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/util/HttpCaching.scala diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala index fbdc4ec..9dcd320 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala @@ -1,14 +1,15 @@ -package com.github.vergenzt.rtmscala.meta +package com.github.vergenzt.rtmscala +package meta import scala.annotation.StaticAnnotation import scala.language.experimental.macros import scala.reflect.macros.whitebox.Context import java.net._ -import com.github.vergenzt.rtmscala._ -import com.github.vergenzt.rtmscala.util._ -import com.github.vergenzt.rtmscala.util.XmlConversions._ import scala.annotation.compileTimeOnly +import util._ +import util.XmlConversions._ + @compileTimeOnly("This is a macro requiring macro paradise.") class GenerateRtmApi extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro GenerateRtmApiImpl.generateRtmApiImpl @@ -27,7 +28,7 @@ object GenerateRtmApiImpl { /** Generate an implementation for RtmApi. */ def generateRtmApiImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ - setUpRtmCaching() + HttpCaching.setUp() annottees.map(_.tree) match { case scala.List(q"object $rtm extends $rtmBase { ..$rtmCustomImpl }") => @@ -57,37 +58,4 @@ object GenerateRtmApiImpl { """) } } - - /** Set up caching of RTM reflection api responses to decrease build times */ - def setUpRtmCaching() = { - import java.net._ - import java.util._ - import java.io._ - ResponseCache.setDefault(new ResponseCache() { - val cacheDir = System.getProperty("user.home") + "/.rtm-scala-meta/build-cache" - new File(cacheDir).mkdirs() - def get(uri: URI, method: String, headers: Map[String, List[String]]) = - try { - val reader = new FileInputStream(cacheDir + "/" + uri.getQuery()) - val headers = new ObjectInputStream(reader).readObject().asInstanceOf[Map[String,List[String]]] - new CacheResponse { - def getBody(): InputStream = reader - def getHeaders(): Map[String,List[String]] = headers - } - } catch { - case _: IOException => null - } - def put(uri: URI, conn: URLConnection) = - try { - val writer = new FileOutputStream(cacheDir + "/" + uri.getQuery(), true) - new ObjectOutputStream(writer).writeObject(conn.getHeaderFields) - new CacheRequest { - def abort(): Unit = writer.close() - def getBody(): OutputStream = writer - } - } catch { - case _: IOException => null - } - }) - } } diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/util/HttpCaching.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/util/HttpCaching.scala new file mode 100644 index 0000000..d420bfb --- /dev/null +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/util/HttpCaching.scala @@ -0,0 +1,37 @@ +package com.github.vergenzt.rtmscala.util + +import java.net._ +import java.util._ +import java.io._ + +object HttpCaching { + /** Set up caching of http responses to decrease build times */ + def setUp() = { + ResponseCache.setDefault(new ResponseCache() { + val cacheDir = System.getProperty("user.home") + "/.rtm-scala-meta/build-cache" + new File(cacheDir).mkdirs() + def get(uri: URI, method: String, headers: Map[String, List[String]]) = + try { + val reader = new FileInputStream(cacheDir + "/" + uri.getQuery()) + val headers = new ObjectInputStream(reader).readObject().asInstanceOf[Map[String,List[String]]] + new CacheResponse { + def getBody(): InputStream = reader + def getHeaders(): Map[String,List[String]] = headers + } + } catch { + case _: IOException => null + } + def put(uri: URI, conn: URLConnection) = + try { + val writer = new FileOutputStream(cacheDir + "/" + uri.getQuery(), true) + new ObjectOutputStream(writer).writeObject(conn.getHeaderFields) + new CacheRequest { + def abort(): Unit = writer.close() + def getBody(): OutputStream = writer + } + } catch { + case _: IOException => null + } + }) + } +} From e0fa18d7bb30e422515dde9dcaa5be39988a7fd6 Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Wed, 17 Feb 2016 23:57:07 -0800 Subject: [PATCH 10/12] Switch to a macro bundle --- .../meta/{generateRtmApi.scala => GenerateRtmApi.scala} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/{generateRtmApi.scala => GenerateRtmApi.scala} (93%) diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala similarity index 93% rename from rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala rename to rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala index 9dcd320..5c26d53 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/generateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala @@ -15,7 +15,8 @@ class GenerateRtmApi extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro GenerateRtmApiImpl.generateRtmApiImpl } -object GenerateRtmApiImpl { +class GenerateRtmApiImpl(val c: Context) { + import c.universe._ implicit val creds = ApiCreds(System.getProperty("rtm.api_key"), System.getProperty("rtm.api_secret")) /* Useful utility methods */ @@ -26,8 +27,7 @@ object GenerateRtmApiImpl { } /** Generate an implementation for RtmApi. */ - def generateRtmApiImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { - import c.universe._ + def generateRtmApiImpl(annottees: c.Expr[Any]*): c.Expr[Any] = { HttpCaching.setUp() annottees.map(_.tree) match { From 8724387c9ab3d29df7f3ae11483dbce36ab11e3d Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Sat, 20 Feb 2016 17:12:16 -0800 Subject: [PATCH 11/12] Eliminate implicit conversion helper --- .../main/scala/com/github/vergenzt/rtmscala/domain.scala | 6 ++++-- .../github/vergenzt/rtmscala/util/XmlConversions.scala | 2 +- .../github/vergenzt/rtmscala/XmlConversionsTest.scala | 2 +- .../github/vergenzt/rtmscala/meta/GenerateRtmApi.scala | 9 +-------- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala index a1e3664..4bb862e 100644 --- a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/domain.scala @@ -124,7 +124,7 @@ case class Transaction( /* Reflection */ case class MethodDesc( - name: String, + fullName: String, needsLogin: Boolean, needsSigning: Boolean, requiredPerms: Permission, @@ -132,7 +132,9 @@ case class MethodDesc( exampleResponse: String, arguments: Seq[ArgumentDesc], errors: Seq[ErrorDesc] -) +) { + val Array("rtm", group, name) = fullName.split("\\.", 3) +} case class ArgumentDesc( name: String, diff --git a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala index 9dbc693..6645df1 100644 --- a/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala +++ b/rtm-scala-lib/src/main/scala/com/github/vergenzt/rtmscala/util/XmlConversions.scala @@ -171,7 +171,7 @@ object XmlConversions { implicit def xml2MethodDesc(xml: NodeSeq): MethodDesc = xml match { case Seq(method @ Node("method", _, body @ _*)) => MethodDesc( - name = method \@ "name", + fullName = method \@ "name", needsLogin = method \@ "needslogin", needsSigning = method \@ "needssigning", requiredPerms = Permission.fromInt((method \@ "requiredperms").toInt).get, diff --git a/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala b/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala index 147eb58..a92828d 100644 --- a/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala +++ b/rtm-scala-lib/src/test/scala/com/github/vergenzt/rtmscala/XmlConversionsTest.scala @@ -263,7 +263,7 @@ class XmlConversionsTest extends FlatSpec with Matchers { """) should be ( MethodDesc( - name = "rtm.test.login", + fullName = "rtm.test.login", needsLogin = true, needsSigning = true, requiredPerms = Permission.Read, diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala index 5c26d53..0f1b4eb 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala @@ -19,13 +19,6 @@ class GenerateRtmApiImpl(val c: Context) { import c.universe._ implicit val creds = ApiCreds(System.getProperty("rtm.api_key"), System.getProperty("rtm.api_secret")) - /* Useful utility methods */ - private implicit class MethodDescExtra(method: MethodDesc) { - def nameParts: Array[String] = method.name.split('.').drop(1) - def group: String = nameParts.head - def methodName: String = nameParts.tail.mkString(".") - } - /** Generate an implementation for RtmApi. */ def generateRtmApiImpl(annottees: c.Expr[Any]*): c.Expr[Any] = { HttpCaching.setUp() @@ -39,7 +32,7 @@ class GenerateRtmApiImpl(val c: Context) { val groupImpls = methods.groupBy(_.group) .map({ case (group, methods) => val methodImpls = methods.map(method => { - q"""def ${TermName(method.methodName)}() = println(${method.methodName})""" + q"""def ${TermName(method.name)}() = println(${method.name})""" }) q""" From d912f5abea0fab3d1d522d5cece936721760641c Mon Sep 17 00:00:00 2001 From: Tim Vergenz Date: Sat, 20 Feb 2016 18:50:59 -0800 Subject: [PATCH 12/12] IT WORKS!!! ...with four temporary differences from the manual implementation: 1. All arguments are accepted as strings. This will be worked around by having a mapping from argument names to domain types. 2. Return values are all HttpRequests; in other words, `.as[T]` must be called manually with the desired return type. This will also be remedied with a mapping, this time from method name to return type. 3. Timelines and Transactions do not have client-side functionality any more. Haven't put any thought into whether or how that will happen yet. 4. `rtm.tasks.notes.*` are not contained in an object `notes` nested within `RtmApi.tasks`. The actual method names are `notes.add`, `.delete`, and `.edit`. Surround the name with backticks to use it from Scala. --- .../rtmscala/meta/GenerateRtmApi.scala | 78 ++++- .../com/github/vergenzt/rtmscala/RtmApi.scala | 302 ------------------ .../github/vergenzt/rtmscala/RtmApiTest.scala | 84 +---- 3 files changed, 67 insertions(+), 397 deletions(-) diff --git a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala index 0f1b4eb..72f421d 100644 --- a/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala +++ b/rtm-scala-meta/src/main/scala/com/github/vergenzt/rtmscala/meta/GenerateRtmApi.scala @@ -19,34 +19,80 @@ class GenerateRtmApiImpl(val c: Context) { import c.universe._ implicit val creds = ApiCreds(System.getProperty("rtm.api_key"), System.getProperty("rtm.api_secret")) + def implementMethod(method: MethodDesc): c.Tree = { + val args = method.arguments + .filter(arg => arg.name != "api_key" && arg.name != "timeline") + .map({ + case arg if !arg.optional => q"val ${TermName(arg.name)}: String" + case arg if arg.optional => q"val ${TermName(arg.name)}: Option[String] = None" + }) + + val implicitArgs = Seq( + Some(q"implicit val creds: ApiCreds"), + Some(q"implicit val authToken: AuthToken") + .filter(_ => method.needsLogin), + Some(q"implicit val timeline: Timeline") + .filter(_ => method.arguments.exists(_.name == "timeline")) + ).flatten + + val body = q""" + val request = unsignedRequest(${method.fullName.stripPrefix("rtm.")}, + ..${method.arguments + .filter(arg => arg.name != "api_key" && arg.name != "timeline") + .map { + // convert each arg into `"" -> value + case arg if !arg.optional => q"${arg.name} -> ${TermName(arg.name)}" + case arg if arg.optional => q"${arg.name} -> ${TermName(arg.name)}.get" + } + }, + ..${Seq( + Some(q"""("auth_token" -> authToken.token)""") + .filter(_ => method.needsLogin), + Some(q"""("timeline" -> timeline.id)""") + .filter(_ => method.arguments.exists(_.name == "timeline")) + ).flatten} + ) + + ${if (method.needsSigning) q"request.signed" else q"request"} + """ + + q"def ${TermName(method.name)} (..$args)(..$implicitArgs) = { $body }" + } + /** Generate an implementation for RtmApi. */ def generateRtmApiImpl(annottees: c.Expr[Any]*): c.Expr[Any] = { HttpCaching.setUp() annottees.map(_.tree) match { - case scala.List(q"object $rtm extends $rtmBase { ..$rtmCustomImpl }") => + case scala.List(q"object $rtm extends RtmApiBase { ..$customImpls }") => val methods = RtmApiBase.reflection.getMethods().filter(_.group != "reflection") - // make an object for each group and an implementation for each method - val groupImpls = methods.groupBy(_.group) - .map({ case (group, methods) => - val methodImpls = methods.map(method => { - q"""def ${TermName(method.name)}() = println(${method.name})""" - }) + c.Expr[Any](q""" + object $rtm extends RtmApiBase { - q""" + // generated method groups + ..${ methods.groupBy(_.group).map { case (group, methods) => q""" object ${TermName(group)} { - ..$methodImpls - } - """ - }) - // TODO merge in the custom methods + // generated methods + ..${methods.map(implementMethod)} - c.Expr[Any](q""" - object $rtm extends $rtmBase { - ..$groupImpls + // custom methods inside generated groups + ..${customImpls.flatMap { + case q"object ${TermName(group)} { ..$customMethods }" => customMethods + case _ => None + }} + } + """}} + + // custom methods outside generated groups + ..${customImpls.filter { + case q"object ${TermName(group)} { ..$impl }" => + !methods.exists(_.group == group) + case _ => + true + }} } """) } diff --git a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala index cea7893..240d0e5 100644 --- a/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala +++ b/rtm-scala/src/main/scala/com/github/vergenzt/rtmscala/RtmApi.scala @@ -27,306 +27,4 @@ import meta._ */ @GenerateRtmApi object RtmApi extends RtmApiBase { - - object auth { - /** - * Get a Remember the Milk authentication URL to direct a user to. - * $perms - * $creds - */ - def getURL(perms: Permission)(implicit creds: ApiCreds) = - baseRequest(AUTH_URL, perms).signed.fullURL - - /** - * Get a Remember the Milk authentication URL to direct a user to. - * $perms - * $frob - * $creds - */ - def getURL(perms: Permission, frob: Frob)(implicit creds: ApiCreds) = - baseRequest(AUTH_URL, perms, frob).signed.fullURL - - /** - * Authenticate the user with Remember the Milk. - * $perms - * @param directUserToURL A function to direct the user to the generated - * authentication URL. Should return a Future that completes once the - * user has finished authenticating. - * $creds - * @param executionContext The context in which to execute the Future. - * @return A Future[AuthToken] that completes once the user has - * authenticated and the resulting token has been fetched. - */ - def authenticate(perms: Permission)(directUserToURL: String => Future[Unit]) - (implicit creds: ApiCreds, executionContext: ExecutionContext): Future[AuthToken] = { - - assert (perms.name != "none") - val frob = getFrob() - directUserToURL(getURL(perms, frob)).map(_ => auth.getToken(frob)) - } - - def checkToken(token: String)(implicit creds: ApiCreds) = - request("auth.checkToken", "auth_token" -> token).as[AuthToken] - - def checkToken(token: AuthToken)(implicit creds: ApiCreds) = - request("auth.checkToken", token).as[AuthToken] - - def getFrob()(implicit creds: ApiCreds) = - request("auth.getFrob").as[Frob] - - def getToken(frob: Frob)(implicit creds: ApiCreds) = - request("auth.getToken", frob).as[AuthToken] - } - - object contacts { - def add(usernameOrEmail: String)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("contacts.add", "contact" -> usernameOrEmail).as[Contact] - - def delete(contact: Contact)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline): Unit = - timelinedRequest("contacts.delete", contact).as[Unit] - - def getList()(implicit creds: ApiCreds, token: AuthToken): Unit = - authedRequest("contacts.getList").as[Seq[Contact]] - } - - object groups { - def add(name: String)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("groups.add", "group" -> name).as[Group] - - def addContact(group: Group, contact: Contact)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline): Unit = - timelinedRequest("groups.addContact", group, contact).as[Unit] - - def delete(group: Group)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline): Unit = - timelinedRequest("groups.delete", group).as[Unit] - - def getList()(implicit creds: ApiCreds, token: AuthToken): Unit = - authedRequest("groups.getList").as[Seq[Group]] - - def removeContact(group: Group, contact: Contact)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline): Unit = - timelinedRequest("groups.removeContact", group, contact).as[Unit] - } - - object lists { - def add(name: String)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.add", "name" -> name).as[List] - - def add(name: String, filter: String)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.add", "name" -> name, "filter" -> filter).as[List] - - def archive(list: List)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.archive", list).as[List] - - def delete(list: List)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.delete", list).as[List] - - def getList()(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("lists.getList").as[Seq[List]] - - def setDefaultList(list: List)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline): Unit = - timelinedRequest("lists.setDefaultList", list).as[Unit] - - def setName(list: List, newName: String)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.setName", list).as[List] - - def unarchive(list: List)(implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("lists.unarchive", list).as[List] - } - - object locations { - def getList()(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("locations.getList").as[Seq[Location]] - } - - object settings { - // TODO: getList - } - - object tasks { - def add(name: String, list: List, parse: Boolean) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.add", "name" -> name, list, "parse" -> parse).as[Task] - - def add(name: String, parse: Boolean = true) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.add", "name" -> name, "parse" -> parse).as[Task] - - def addTags(task: Task, tags: Seq[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.addTags", ("tags" -> tags.mkString(",")) :: task.toList: _*).as[Task] - - def complete(task: Task) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.complete", task: _*).as[Task] - - def delete(task: Task) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.delete", task: _*).as[Unit] - - def getList()(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("tasks.getList").as[Seq[Task]] - - def getList(since: DateTime)(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("tasks.getList", "last_sync" -> since.toString(util.rtmDateTimeFormat)).as[Seq[Task]] - - def getList(filter: String)(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("tasks.getList", "filter" -> filter).as[Seq[Task]] - - def getList(list: List, filter: String)(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("tasks.getList", list, "filter" -> filter).as[Seq[Task]] - - def movePriority(task: Task, direction: String) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = { - require(direction == "up" || direction == "down") - timelinedRequest("tasks.addTags", ("direction" -> direction) :: task.toList: _*).as[Task] - } - - def moveTo(task: Task, list: List) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = { - timelinedRequest("tasks.moveTo", - "task_id" -> task.id, "taskseries_id" -> task.seriesId, - "from_list_id" -> task.listId, - "to_list_id" -> list.id - ).as[Task] - - } - - def postpone(task: Task) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.postpone", task: _*).as[Task] - - def removeTags(task: Task, tags: Seq[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.removeTags", - ("tags" -> tags.mkString(",")) :: - task.toList: _* - ).as[Task] - - def setDueDate(task: Task, due: String) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setDueDate", ("parse" -> "1") :: ("due" -> due) :: task.toList: _*).as[Task] - - def setDueDate(task: Task, due: LocalDate) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setDueDate", - ("due" -> due.toString(rtmDateTimeFormat)) :: - task.toList: _* - ).as[Task] - - def setDueDate(task: Task, due: LocalDateTime) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setDueDate", - ("due" -> due.toString(rtmDateTimeFormat)) :: - ("has_due_time" -> "1") :: - task.toList: _* - ).as[Task] - - def unsetDueDate(task: Task) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setDueDate", task: _*).as[Task] - - def setEstimate(task: Task, estimate: Option[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setEstimate", - ("estimate" -> estimate.getOrElse("")) :: - task.toList: _* - ).as[Task] - - def setLocation(task: Task, location: Option[Location]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setEstimate", - ("location_id" -> location.map(_.id).getOrElse("")) :: - task.toList: _* - ).as[Task] - - def setName(task: Task, newName: String) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setName", - ("name" -> newName) :: - task.toList: _* - ).as[Task] - - def setPriority(task: Task, newPriority: Int) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = { - require (0 <= newPriority && newPriority <= 4) - timelinedRequest("tasks.setName", - ("name" -> newPriority.toString) :: - task.toList: _* - ).as[Task] - } - - def setRecurrence(task: Task, recurrence: Option[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setName", - ("repeat" -> recurrence.getOrElse("")) :: - task.toList: _* - ).as[Task] - - def setTags(task: Task, tags: Seq[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setTags", - ("tags" -> tags.mkString(",")) :: - task.toList: _* - ).as[Task] - - def setURL(task: Task, url: Option[String]) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.setName", - ("url" -> url.getOrElse("")) :: - task.toList: _* - ).as[Task] - - def uncomplete(task: Task) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.uncomplete", task: _*).as[Task] - - object notes { - def add(task: Task, title: String, body: String) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.notes.add", - ("note_title" -> title) :: - ("note_text" -> body) :: - task.toList: _* - ).as[Note] - - def delete(note: Note) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.notes.delete", note).as[Note] - - def edit(note: Note, newTitle: String, newBody: String) - (implicit creds: ApiCreds, token: AuthToken, timeline: Timeline) = - timelinedRequest("tasks.notes.edit", - note, "note_title" -> newTitle, "note_text" -> newBody - ).as[Note] - } - } - - object test { - def echo(params: (String, String)*)(implicit creds: ApiCreds) = - unsignedRequest("test.echo", params: _*).asString.body - - def login(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("test.login").as[AuthToken] - } - - object time { - // TODO: convert - // TODO: parse - } - - object timelines { - def create(implicit creds: ApiCreds, token: AuthToken) = - authedRequest("timelines.create").as[Timeline] - } - - object timezones { - // TODO: getList - } - - object transactions { - def undo(transaction: Transaction)(implicit creds: ApiCreds, token: AuthToken) = { - authedRequest("transactions.undo", transaction.timeline, transaction).as[Unit] - // remove the transaction from the timeline - transaction.timeline._transactions -= transaction - } - } } diff --git a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala index cd0c57c..50f0597 100644 --- a/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala +++ b/rtm-scala/src/test/scala/com/github/vergenzt/rtmscala/RtmApiTest.scala @@ -25,7 +25,7 @@ class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with Scal before { server = new MockWebServer() server.start() - rtm.REST_URL = server.getUrl("/rest/").toExternalForm() + RtmApi.REST_URL = server.getUrl("/rest/").toExternalForm() } after { server.shutdown() @@ -50,69 +50,11 @@ class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with Scal implicit val authToken = AuthToken("314159", Permission.Delete, User("1", "bob", Some("Bob T. Monkey"))) implicit val timeline = Timeline("54321") - describe("test_dynamic") { - it("works") { - rtm.test - } - } - describe("auth") { - - it("signs correctly") { - import util.RtmHttpRequestOps - - val request = rtm.request("sigtest", "param1" -> "xyz", "param2" -> "123") - - val params = request.params.toMap - val _params = Map( - "api_key" -> apiCreds.apiKey, - "method" -> "rtm.sigtest", - "param1" -> "xyz", - "param2" -> "123", - "api_sig" -> "02c04e204a9d7f4aad86468e74412f50" - ) - _params.foreach { case (k,v) => - assert (params.get(k) == Some(v)) - } - } - - it("getFrob") { - enqueueResponse("""123456""") - - val frob = rtm.auth.getFrob - assert (frob == Frob("123456")) - - checkParamsIncluded(server.takeRequest, Map( - "api_key" -> apiCreds.apiKey, - "method" -> "rtm.auth.getFrob", - "api_sig" -> "2eb41243b94f6be134b1120623ca6876" - )) - } - - it("getToken") { - enqueueResponse(""" - - 314159 - delete - - - """.trim) - - assert ( - rtm.auth.getToken(Frob("123456")) - == - AuthToken("314159", Permission.Delete, User("1", "bob", Some("Bob T. Monkey"))) - ) - - checkParamsIncluded(server.takeRequest, Map( - "api_key" -> apiCreds.apiKey, - "method" -> "rtm.auth.getToken", - "frob" -> "123456", - "api_sig" -> "6dd557b1cbb1725334fa513760a75cdd" - )) - } - it("checkToken") { + import util._ + import XmlConversions._ + enqueueResponse(s""" ${authToken.token} @@ -121,7 +63,7 @@ class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with Scal """.trim) - assert (rtm.auth.checkToken(authToken) == authToken) + assert (rtm.auth.checkToken(authToken.token).as[AuthToken] == authToken) checkParamsIncluded(server.takeRequest, Map( "api_key" -> apiCreds.apiKey, @@ -130,21 +72,5 @@ class RtmApiTest extends FunSpec with BeforeAndAfter with MockitoSugar with Scal )) } } - - describe("timelines") { - it("create") { - enqueueResponse(s""" - ${timeline.id} - """.trim) - - assert (rtm.timelines.create == timeline) - - checkParamsIncluded(server.takeRequest, Map( - "api_key" -> apiCreds.apiKey, - "method" -> "rtm.timelines.create", - "auth_token" -> authToken.token - )) - } - } } }