├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── docs
└── src
│ ├── main
│ └── tut
│ │ ├── 01-installation.md
│ │ ├── 02-basic-usage.md
│ │ ├── 03-content-types-and-encoders.md
│ │ ├── 04-response-decoding-and-validation.md
│ │ ├── 05-error-handling.md
│ │ ├── 06-building-rest-clients.md
│ │ └── index.md
│ └── site
│ ├── _config.yml
│ ├── _includes
│ ├── head.html
│ ├── page-footer.html
│ └── page-header.html
│ ├── _layouts
│ ├── default.html
│ └── post.html
│ ├── css
│ ├── cayman.css
│ ├── code.css
│ └── normalize.css
│ └── index.md
├── featherbed-circe
├── build.sbt
└── src
│ ├── main
│ └── scala
│ │ └── featherbed
│ │ └── circe
│ │ └── package.scala
│ └── test
│ └── scala
│ └── featherbed
│ └── circe
│ └── CirceSpec.scala
├── featherbed-core
└── src
│ ├── main
│ └── scala
│ │ └── featherbed
│ │ ├── Client.scala
│ │ ├── content
│ │ └── package.scala
│ │ ├── littlemacros
│ │ └── CoproductMacros.scala
│ │ ├── request
│ │ ├── CanBuildRequest.scala
│ │ └── RequestSyntax.scala
│ │ └── support
│ │ ├── AcceptHeader.scala
│ │ ├── ContentType.scala
│ │ └── package.scala
│ └── test
│ └── scala
│ └── featherbed
│ ├── ClientSpec.scala
│ ├── ErrorHandlingSpec.scala
│ └── fixture
│ └── package.scala
├── project
├── build.properties
└── plugins.sbt
└── scalastyle-config.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | .idea
3 | .DS_Store
4 | doc
5 | .ensime
6 | .ensime_cache/*
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: scala
3 | scala:
4 | - 2.11.11
5 | - 2.12.2
6 | jdk:
7 | - oraclejdk8
8 |
9 | script:
10 | - sbt ++$TRAVIS_SCALA_VERSION validate
11 | - sbt ++$TRAVIS_SCALA_VERSION coverageAggregate
12 |
13 | cache:
14 | directories:
15 | - $HOME/.ivy2/cache
16 | - $HOME/.sbt/boot/
17 |
18 | before_cache:
19 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete
20 | - find $HOME/.sbt -name "*.lock" -delete
21 |
22 |
23 | after_success:
24 | - bash <(curl -s https://codecov.io/bash)
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # featherbed
2 |
3 | [](https://gitter.im/finagle/featherbed?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/finagle/featherbed)
5 | [](https://codecov.io/gh/finagle/featherbed)
6 | [](https://maven-badges.herokuapp.com/maven-central/io.github.finagle/featherbed-core_2.12)
7 |
8 | Featherbed aims to be a typesafe, functional REST client API over [Finagle](https://github.com/twitter/finagle).
9 | It provides a friendlier approach to building REST client interfaces in Scala. Currently, Featherbed
10 | is in the early stages of development, and includes the following modules:
11 |
12 | 1. `featherbed-core` - the functional client interface
13 | 2. `featherbed-circe` - automatic JSON request encoding and response decoding using [circe](https://github.com/travisbrown/circe)
14 |
15 | The following modules are planned:
16 |
17 | 1. `featherbed-oauth` - OAuth authenticated requests
18 |
19 | ## Documentation
20 | To get started with featherbed, check out the [Guide](https://finagle.github.io/featherbed/doc/).
21 |
22 | ## Dependencies
23 |
24 | Featherbed aims to have a minimal set of dependencies. Besides `finagle-http`, the core project is
25 | dependent only on [shapeless](https://github.com/milessabin/shapeless) and [cats](https://github.com/typelevel/cats).
26 |
27 | featherbed-circe depends additionall on [circe](https://github.com/travisbrown/circe)
28 |
29 | ## License
30 |
31 | Featherbed is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
32 | (the "License"); you may not use this software except in compliance with the License.
33 |
34 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
35 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
36 | language governing permissions and limitations under the License.
37 |
38 | ### Dependency Licenses
39 |
40 | As of the latest build of featherbed,
41 |
42 | * [Finagle](https://github.com/twitter/finagle) is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
43 | * [shapeless](https://github.com/milessabin/shapeless) is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
44 | * [cats](https://github.com/typelevel/cats) is licensed under the [MIT License](http://opensource.org/licenses/mit-license.php)
45 | * [circe](https://github.com/travisbrown/circe) is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
46 |
47 | Featherbed is an independent project, and is neither governed nor endorsed by any of the above projects. All uses of
48 | the above projects by featherbed are within those allowed by each respective project's license. Any software using
49 | one of the above projects, even as a dependency of featherbed, must also abide by that project's license in addition to
50 | featherbed's.
51 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "featherbed"
2 |
3 | import sbtunidoc.Plugin.UnidocKeys._
4 | import com.typesafe.sbt.SbtGhPages.GhPagesKeys._
5 |
6 | enablePlugins(TutPlugin)
7 |
8 | lazy val buildSettings = Seq(
9 | organization := "io.github.finagle",
10 | version := "0.3.3",
11 | scalaVersion := "2.12.2",
12 | crossScalaVersions := Seq("2.11.11", "2.12.2")
13 | )
14 |
15 | val finagleVersion = "17.12.0"
16 | val shapelessVersion = "2.3.3"
17 | val catsVersion = "1.0.1"
18 |
19 | lazy val docSettings = Seq(
20 | autoAPIMappings := true
21 | )
22 |
23 | lazy val baseSettings = docSettings ++ Seq(
24 | libraryDependencies ++= Seq(
25 | "com.twitter" %% "finagle-http" % finagleVersion,
26 | "com.chuusai" %% "shapeless" % shapelessVersion,
27 | "org.typelevel" %% "cats-core" % catsVersion,
28 | "org.scalamock" %% "scalamock-scalatest-support" % "3.6.0" % "test",
29 | "org.scalatest" %% "scalatest" % "3.0.3" % "test"
30 | ),
31 | resolvers += Resolver.sonatypeRepo("snapshots"),
32 | dependencyUpdatesFailBuild := false,
33 | dependencyUpdatesExclusions := moduleFilter("org.scala-lang")
34 | )
35 |
36 | lazy val publishSettings = Seq(
37 | publishMavenStyle := true,
38 | publishArtifact := true,
39 | publishTo := {
40 | val nexus = "https://oss.sonatype.org/"
41 | if (isSnapshot.value)
42 | Some("snapshots" at nexus + "content/repositories/snapshots")
43 | else
44 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
45 | },
46 | publishArtifact in Test := false,
47 | licenses := Seq("Apache 2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
48 | homepage := Some(url("https://finagle.github.io/featherbed/")),
49 | autoAPIMappings := true,
50 | scmInfo := Some(
51 | ScmInfo(
52 | url("https://github.com/finagle/featherbed"),
53 | "scm:git:git@github.com:finagle/featherbed.git"
54 | )
55 | ),
56 | pomExtra :=
57 |
58 |
59 | jeremyrsmith
60 | Jeremy Smith
61 | https://github.com/jeremyrsmith
62 |
63 |
64 | )
65 |
66 | lazy val noPublish = Seq(
67 | publish := {},
68 | publishLocal := {},
69 | publishArtifact := false
70 | )
71 |
72 | lazy val allSettings = publishSettings ++ baseSettings ++ buildSettings
73 |
74 | lazy val `featherbed-core` = project
75 | .settings(allSettings)
76 |
77 | lazy val `featherbed-circe` = project
78 | .settings(allSettings)
79 | .dependsOn(`featherbed-core`)
80 |
81 | val scaladocVersionPath = settingKey[String]("Path to this version's ScalaDoc")
82 | val scaladocLatestPath = settingKey[String]("Path to latest ScalaDoc")
83 | val tutPath = settingKey[String]("Path to tutorials")
84 |
85 | lazy val docs: Project = project
86 | .enablePlugins(TutPlugin)
87 | .settings(
88 | allSettings ++ ghpages.settings ++ Seq(
89 | scaladocVersionPath := ("api/" + version.value),
90 | scaladocLatestPath := (if (isSnapshot.value) "api/latest-snapshot" else "api/latest"),
91 | tutPath := "doc",
92 | includeFilter in makeSite := (includeFilter in makeSite).value || "*.md" || "*.yml",
93 | addMappingsToSiteDir(tut, tutPath),
94 | addMappingsToSiteDir(mappings in (featherbed, ScalaUnidoc, packageDoc), scaladocLatestPath),
95 | addMappingsToSiteDir(mappings in (featherbed, ScalaUnidoc, packageDoc), scaladocVersionPath),
96 | ghpagesNoJekyll := false,
97 | git.remoteRepo := "git@github.com:finagle/featherbed",
98 | scalacOptions in Tut := (
99 | CrossVersion.partialVersion(scalaVersion.value) match {
100 | case Some((2, p)) if p >= 12 => Seq("-Yrepl-class-based")
101 | case _ => Nil
102 | }
103 | )
104 | )
105 | ).dependsOn(`featherbed-core`, `featherbed-circe`)
106 |
107 |
108 | lazy val featherbed = project
109 | .in(file("."))
110 | .settings(unidocSettings ++ baseSettings ++ buildSettings ++ publishSettings)
111 | .aggregate(`featherbed-core`, `featherbed-circe`)
112 | .dependsOn(`featherbed-core`, `featherbed-circe`)
113 | .settings(
114 | initialCommands in console :=
115 | """
116 | |import com.twitter.util.{Await, Future}
117 | |import com.twitter.finagle.{Service, Http}
118 | |import com.twitter.finagle.http.{Request, Response, Method}
119 | |import java.net.{InetSocketAddress, URL}
120 | |import shapeless.Coproduct
121 | |import featherbed._
122 | |import featherbed.circe._
123 | |import io.circe.generic.auto._
124 | """.stripMargin
125 | )
126 |
127 | val validateCommands = List(
128 | "dependencyUpdates",
129 | "clean",
130 | "scalastyle",
131 | "test:scalastyle",
132 | "compile",
133 | "test:compile",
134 | "coverage",
135 | "test",
136 | "docs/tut",
137 | "coverageReport"
138 | )
139 | addCommandAlias("validate", validateCommands.mkString(";", ";", ""))
140 |
--------------------------------------------------------------------------------
/docs/src/main/tut/01-installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | layout: default
4 | ---
5 |
6 | # Installation
7 |
8 | Add the following to build.sbt
9 |
10 | ```scala
11 | resolvers += Resolver.sonatypeRepo("snapshots")
12 |
13 | libraryDependencies ++= Seq(
14 | "io.github.finagle" %"featherbed_2.11" %"0.3.0"
15 | )
16 | ```
17 | Next, read about [Basic Usage](02-basic-usage.html)
18 |
--------------------------------------------------------------------------------
/docs/src/main/tut/02-basic-usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Basic Usage
3 | layout: default
4 | ---
5 |
6 | # Basic Usage
7 |
8 | Assuming an HTTP server exists at `localhost:8765`:
9 |
10 | ```tut:book
11 |
12 | // set up a dummy HTTP service on port 8675 that just echoes some request information
13 |
14 | import com.twitter.util.Future
15 | import com.twitter.finagle.{Service,Http}
16 | import com.twitter.finagle.http.{Request,Response,Method}
17 | import java.net.InetSocketAddress
18 | val server = Http.serve(new InetSocketAddress(8765), new Service[Request, Response] {
19 | def apply(request: Request): Future[Response] = Future {
20 | val rep = Response()
21 | rep.headerMap.put("X-Foo", "Bar")
22 | if(request.method != Method.Head)
23 | rep.contentString = s"${request.method} ${request.uri} :: ${request.contentString}"
24 | rep
25 | }
26 | })
27 | ```
28 |
29 | Create a `Client`, passing the base URL to the REST endpoints:
30 |
31 | ```tut:book
32 | import java.net.URL
33 | val client = new featherbed.Client(new URL("http://localhost:8765/api/"))
34 | ```
35 | *Note:* It is important to put a trailing slash on your URL. This is because the resource path you'll pass in below
36 | is evaluated as a relative URL to the base URL given. Without a trailing slash, the `api` directory above would be
37 | lost when the relative URL is resolved.
38 |
39 | Now you can make some requests:
40 |
41 | ```tut:book
42 | import com.twitter.util.Await
43 |
44 | Await.result {
45 | val request = client.get("test/resource").send[Response]()
46 | request map {
47 | response => response.contentString
48 | }
49 | }
50 | ```
51 |
52 | The result of making a request is a `Future` that must be mapped (or `flatMap`ped) over. Normally, you wouldn't use
53 | `Await` in real code, because you don't want to block Finagle's event loop. But here, it demonstrates the result of
54 | mapping the `Future[Response]` to a `Future[String]` which will contain the response's content.
55 |
56 | Besides `get`, the other REST verbs are also available; the process of specifying a request has a fluent API which
57 | can be used to fine-tune the request that will be sent.
58 |
59 | Here's an example of using a `POST` request to submit a web form-style request:
60 |
61 | ```tut:book
62 | import java.nio.charset.StandardCharsets._
63 |
64 | Await.result {
65 | client
66 | .post("another/resource")
67 | .withParams(
68 | "foo" -> "foz",
69 | "bar" -> "baz")
70 | .withCharset(UTF_8)
71 | .withHeaders("X-Foo" -> "scooby-doo")
72 | .send[Response]()
73 | .map {
74 | response => response.contentString
75 | }
76 | }
77 | ```
78 |
79 | Here's how you might send a `HEAD` request (note the lack of a type argument to `send()` for a HEAD request):
80 |
81 | ```tut:book
82 | Await.result {
83 | client.head("head/request").send().map(_.headerMap)
84 | }
85 | ```
86 |
87 | A `DELETE` request:
88 |
89 | ```tut:book
90 | Await.result {
91 | client.delete("delete/request").send[Response]() map {
92 | response => response.statusCode
93 | }
94 | }
95 | ```
96 |
97 | And a `PUT` request - notice how content can be provided to a `PUT` request by giving it a `Buf` buffer and a MIME type
98 | to serve as the `Content-Type`:
99 |
100 | ```tut:book
101 | import com.twitter.io.Buf
102 |
103 | Await.result {
104 | client.put("put/request")
105 | .withContent(Buf.Utf8("Hello world!"), "text/plain")
106 | .send[Response]()
107 | .map {
108 | response => response.statusCode
109 | }
110 | }
111 | ```
112 |
113 | You can also provide content to a `POST` request in the same fashion:
114 |
115 | ```tut:book
116 | import com.twitter.io.Buf
117 |
118 | Await.result {
119 | client.post("another/post/request")
120 | .withContent(Buf.Utf8("Hello world!"), "text/plain")
121 | .send[Response]()
122 | .map {
123 | response => response.contentString
124 | }
125 | }
126 | ```
127 |
128 | ```tut:invisible
129 | Await.result(server.close())
130 | ```
131 |
132 | Using a `Buf` for content enables specifying low-level content, but you're usually going to want to use a more
133 | high-level interface to interact with a REST service. To see how that works, read about
134 | [Content types and Encoders](03-content-types-and-encoders.html).
135 |
--------------------------------------------------------------------------------
/docs/src/main/tut/03-content-types-and-encoders.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Content Types and Encoders
3 | layout: default
4 | ---
5 |
6 | # Content Types and Encoders
7 |
8 | In many cases, you'll have to send content to REST services. Most of the time, you aren't going to want to pass a `Buf`
9 | (a collection of bytes) to a REST service. Rather, you would probably prefer to pass some representation of a request,
10 | and featherbed is equipped to handle that. As long as there is a `featherbed.content.Encoder[T, CT]` in implicit
11 | scope, featherbed can take care of marshalling a value of type `T` into a representation in MIME type `CT`.
12 |
13 | If that sounds confusing, don't worry. Featherbed provides modules for dealing with common content types. If you want
14 | to implement a content type for yourself, you can read about it later on (it's not that hard, as long as you understand
15 | typeclasses and singleton literals.)
16 |
17 | Let's take a look at how we might interact with a service that accepts JSON payloads. We'll use the provided module
18 | `featherbed-circe`, which provides automatic JSON encoding and decoding using the excellent
19 | [Circe](https://github.com/travisbrown/circe) library from the Typelevel stack.
20 |
21 | First, the same setup as before:
22 |
23 | ```tut:book
24 | import com.twitter.util.{Future,Await}
25 | import com.twitter.finagle.{Service,Http}
26 | import com.twitter.finagle.http.{Request,Response}
27 | import java.net.InetSocketAddress
28 |
29 | val server = Http.serve(new InetSocketAddress(8766), new Service[Request, Response] {
30 | def apply(request: Request): Future[Response] = Future {
31 | val rep = Response()
32 | rep.contentString = s"${request.method} ${request.uri} :: ${request.contentString}"
33 | rep
34 | }
35 | })
36 |
37 | import java.net.URL
38 | val client = new featherbed.Client(new URL("http://localhost:8766/api/"))
39 | ```
40 |
41 | Importing `featherbed.circe._` brings an implicit derivation from `io.circe.Encoder[A]` to
42 | `featherbed.content.Encoder[A, "application/json"]`. As long as there is a Circe `Encoder[A]`
43 | in implicit scope, we will be able to pass `A` directly as content in featherbed requests:
44 |
45 | ```tut:book
46 | import io.circe.generic.auto._
47 | import featherbed.circe._
48 |
49 | // An ADT for the request
50 | case class Foo(someText : String, someInt : Int)
51 |
52 | // It can be passed directly to the POST
53 | val req = client.post("foo/bar").withContent(Foo("Hello world!", 42), "application/json")
54 |
55 | val result = Await.result {
56 | req.send[Response]() map {
57 | response => response.contentString
58 | }
59 | }
60 | ```
61 |
62 | ```tut:invisible
63 | Await.result(server.close())
64 | ```
65 |
66 | Here we used `io.circe.generic.auto._` to automatically derive a JSON codec for `Foo` - but if you have need to encode
67 | a particular data type into JSON in a certain way, you can also specify an implicit Circe `Encoder` value in the data
68 | type's companion object. See the Circe documentation for more details about JSON encoding and decoding.
69 |
70 | ### A Note About Evaluation
71 |
72 | You may have noticed that above we created a value called `req`, which held the result of specifying the request
73 | type and its parameters. We later called `send[Response]` on that value and `map`ped over the result to specify a
74 | transformation of the response.
75 |
76 | It's important to note that the request itself **is not performed** until the call to `send`. Until that call is made,
77 | you will have an instance of some kind of request, but you will not have a `Future` representing the response. That is,
78 | the request itself is *lazy*. The reason this is important to note is that `req` itself can actually be used to make
79 | the same request again. If another call is made to `send`, a new request of the same parameters will be initiated and a
80 | new `Future` will be returned. This can be a useful and powerful thing, but it can also bite you if you're unaware.
81 |
82 | For more information about lazy tasks, take a look at scalaz's `Task` or cats's `Eval`. Again, this is important to
83 | note, and is different than what people are used to with Finagle's `Future` (which is not lazy).
84 |
85 | ### A Note About Types
86 |
87 | You may have also noticed that we specified a content type string, `"application/json"`. From this, the request knew
88 | to encode the `Foo` object as JSON. It may not seem obvious, but this decision was actually made *at compile time*.
89 | When `featherbed.circe._` was imported, we gained a typelevel specification that requests being made with
90 | "application/json" can be encoded as long as the payload's type has an available Circe `Encoder`. This is accomplished
91 | by treating `"application/json"` as a value of *type* `"application/string"` rather than a value of type `String`. For
92 | more information about singleton literals and their (amazing) implications, check out some of the projects in
93 | Typelevel Scala (particularly Shapeless). Scala can do some amazing things (but it does need a little help once in a while.)
94 |
95 | Next, read about [Response Decoding and Validation](04-response-decoding-and-validation.html)
96 |
--------------------------------------------------------------------------------
/docs/src/main/tut/04-response-decoding-and-validation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Response Decoding and Validation
3 | layout: default
4 | ---
5 |
6 | # Response Decoding and Validation
7 |
8 | In the previous section, we looked at how we can import `Encoder` instances to make it so we
9 | can treat case classes as HTTP request content. We've also seen how a request specification
10 | can be sent using `send[Response]` in order to send the request and create a `Future` representing
11 | the response.
12 |
13 | Once you have a `Future[Response]`, what then? Of course, the `Response` will be more useful if
14 | it's transformed into some typed data. In a similar fashion to `Encoder`, we can make use of
15 | an implicit `Decoder` to accomplish this.
16 |
17 | First, the setup. This time, our dummy server is going to be a little more complicated: for
18 | requests to `/foo/good`, it will return the same JSON that we give it with an `application/json`
19 | Content-Type. For requests to `/foo/bad`, it will return some invalid JSON with an `application/json`
20 | Content-Type. For requests to `/foo/awful`, it will return some junk with a completely
21 | unexpected Content-Type.
22 |
23 | ```tut:book
24 | import com.twitter.util.{Future,Await}
25 | import com.twitter.finagle.{Service,Http}
26 | import com.twitter.finagle.http.{Request,Response}
27 | import java.net.InetSocketAddress
28 |
29 | val server = Http.serve(new InetSocketAddress(8767), new Service[Request, Response] {
30 | def apply(request: Request): Future[Response] = request.uri match {
31 | case "/api/foo/good" => Future {
32 | val rep = Response()
33 | rep.contentString = request.contentString
34 | rep.setContentTypeJson()
35 | rep
36 | }
37 | case "/api/foo/bad" => Future {
38 | val rep = Response()
39 | rep.contentString = "This text is not valid JSON!"
40 | rep.setContentTypeJson()
41 | rep
42 | }
43 | case "/api/foo/awful" => Future {
44 | val rep = Response()
45 | rep.contentString = "This text is not valid anything!"
46 | rep.setContentType("pie/pumpkin", "UTF-8")
47 | rep
48 | }
49 | }
50 | })
51 |
52 | import java.net.URL
53 | val client = new featherbed.Client(new URL("http://localhost:8767/api/"))
54 | ```
55 |
56 | To specify that a response should be decoded, use the `send[T]` method to initiate the request:
57 |
58 | ```tut:book:nofail
59 | import featherbed.circe._
60 | import io.circe.generic.auto._
61 |
62 | case class Foo(someText: String, someInt: Int)
63 |
64 | Await.result {
65 | val request = client.post("foo/good").withContent(Foo("Hello world", 42), "application/json")
66 | request.send[Foo]()
67 | }
68 | ```
69 |
70 | Oops! What happened? Like the error message explains, we can't compile that code because we have
71 | to specify an `Accept` header and ensure that we're able to decode all of the types we specify
72 | into `Foo`. In scala type land, the `Accept` content types are a `Coproduct` of string literals
73 | which can be specified using shapless's `Coproduct` syntax. In this case, we only want `application/json`.
74 |
75 | ```tut:book
76 | import shapeless.Coproduct
77 |
78 | // Specifies only "application/json" as an acceptable response type
79 | val example1 = client.post(
80 | "foo/good"
81 | ).withContent(
82 | Foo("Hello world", 42), "application/json"
83 | ).accept[Coproduct.`"application/json"`.T]
84 |
85 | // Specifies that both "application/json" and "text/xml" are acceptable
86 | // Note that Featherbed doesn't currently ship with an XML decoder; it's just for sake of example.
87 | val example2 = client.post(
88 | "foo/good"
89 | ).withContent(
90 | Foo("Hello world", 42), "application/json"
91 | ).accept[Coproduct.`"application/json", "text/xml"`.T]
92 |
93 | ```
94 |
95 | That ``Coproduct.`"a", "b"`.T `` syntax is specifying a *type* that encompasses the possible response MIME types that
96 | the request will handle. If you think the syntax is a little bit ugly, you're right! There's an alternative syntax:
97 |
98 | ```tut:book
99 | val example3 = client.post(
100 | "foo/good"
101 | ).withContent(
102 | Foo("Hello world", 42), "application/json"
103 | ).accept("application/json", "text/xml")
104 | ```
105 |
106 | This uses a small macro to lift those `String` arguments into a `Coproduct` type, which looks a lot nicer and more
107 | readable. However, Scala sometimes has trouble inferring that type when subsequent methods are called on the request,
108 | so make sure you call `accept` last when using that syntax.
109 |
110 | Let's make an actual request, again using only `"application/json"` (since we have a decoder for that from circe):
111 |
112 | ```tut:book
113 | import shapeless.Coproduct
114 |
115 | Await.result {
116 | val request = client.post("foo/good")
117 | .withContent(Foo("Hello world", 42), "application/json")
118 | .accept("application/json")
119 |
120 | request.send[Foo]()
121 | }
122 | ```
123 |
124 | Look at that! The JSON that came back was automatically decoded into a `Foo`! But what's that `Valid`
125 | thing around it? As we're about to see, when you're interacting with a server, you can't be sure that
126 | you'll get what you expect. The server might send malformed JSON, or might not send JSON at all. To
127 | handle this in an idiomatic way, the `Future` returned by `send[K]` will fail with `InvalidResponse` if
128 | the response can't be decoded. The `InvalidResponse` contains a message about why the response was invalid,
129 | as well as the `Response` itself (so you can process it further if you like).
130 |
131 | Let's see what that looks like:
132 |
133 | ```tut:book:nofail
134 | Await.result {
135 | val request = client.post("foo/bad")
136 | .withContent(Foo("Hello world", 42), "application/json")
137 | .accept("application/json")
138 |
139 | request.send[Foo]()
140 | }
141 | ```
142 |
143 | Here, since we didn't handle the `InvalidResponse`, awaiting the future resulted in an exception being thrown. Instead,
144 | you can `handle` the failed future and recover in some way. A typical pattern is to capture the error in something like
145 | `Either``:
146 |
147 | ```tut:book
148 | import featherbed.request.InvalidResponse
149 |
150 | Await.result {
151 | val request = client.post("foo/bad")
152 | .withContent(Foo("Hello world", 42), "application/json")
153 | .accept("application/json")
154 |
155 | request.send[Foo]().map(Right.apply).handle {
156 | case err @ InvalidResponse(rep, reason) => Left(err)
157 | }
158 | }
159 | ```
160 |
161 | This example maps the `Future`'s successful result into a `Right`, and the `InvalidResponse` case into a `Left`,
162 | which represents the failure. The `Either` can be handled further by the application.
163 |
164 | Alternatively, you might want to use some default `Foo` in the event that the response can't be decoded:
165 |
166 | ```tut:book
167 | Await.result {
168 | val request = client.post("foo/bad")
169 | .withContent(Foo("Hello world", 42), "application/json")
170 | .accept("application/json")
171 |
172 | request.send[Foo]().map(Right.apply).handle {
173 | case InvalidResponse(rep, reason) =>
174 | println(s"ERROR: response decoding failed: $reason")
175 | Foo("Default", 0)
176 | }
177 | }
178 | ```
179 |
180 | Similarly, if the response's content-type isn't one of the accepted MIME types, a different `InvalidResponse` is given:
181 |
182 | ```tut:book:nofail
183 | Await.result {
184 | val request = client.post("foo/awful")
185 | .withContent(Foo("Hello world", 42), "application/json")
186 | .accept("application/json")
187 |
188 | request.send[Foo]()
189 | }
190 | ```
191 |
192 | ```tut:invisible
193 | Await.result(server.close())
194 | ```
195 |
196 | As you can see, these different failure scenarios provide different messages about what failure occured,
197 | and give the original `Response`. In the first case, we get back Circe's parsing error. In the second
198 | case, we get a message that the content type wasn't expected and therefore there isn't a decoder for it.
199 | This helps us deal with inevitable runtime failures resulting from external systems.
200 |
201 | Next, read about [Error Handling](05-error-handling.html)
202 |
--------------------------------------------------------------------------------
/docs/src/main/tut/05-error-handling.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Error Handling
3 | layout: default
4 | ---
5 |
6 | # Error Handling
7 |
8 | In the [previous section](04-response-decoding-and-validation.html), we saw how we can decode responses automatically
9 | to an algebraic data type using `send[T]`. We also saw how the decoded response is wrapped in a `Validated` data type,
10 | to capture any failures in decoding the response.
11 |
12 | Requests can fail for other reasons besides malformed responses, though. What happens if the server returns an HTTP
13 | error, like `404 Not Found` or `401 Unauthorized`? Let's set up another dummy server that returns errors, so we can
14 | explore the various ways of handling them:
15 |
16 | ```tut:book
17 | import com.twitter.util.{Future,Await}
18 | import com.twitter.finagle.{Service,Http}
19 | import com.twitter.finagle.http.{Request,Response,Status,Version}
20 | import java.net.{URL, InetSocketAddress}
21 | import featherbed.request.ErrorResponse
22 | import featherbed.circe._
23 | import io.circe.generic.auto._
24 |
25 | val server = Http.serve(new InetSocketAddress(8768), new Service[Request, Response] {
26 | def response(status: Status, content: String) = {
27 | val rep = Response(Version.Http11, status)
28 | rep.contentString = content
29 | rep.contentType = "application/json"
30 | Future.value(rep)
31 | }
32 |
33 | def apply(request: Request): Future[Response] = request.uri match {
34 | case "/api/success" => response(Status.Ok, """{"foo": "bar"}""")
35 | case "/api/not/found" => response(
36 | Status.NotFound,
37 | """{"error": "The thing couldn't be found"}"""
38 | )
39 | case "/api/unauthorized" => response(
40 | Status.Unauthorized,
41 | """{"error": "Not authorized to access the thing"}"""
42 | )
43 | case "/api/server/error" => response(
44 | Status.InternalServerError,
45 | """{"error": "Something went terribly wrong"}"""
46 | )
47 | }
48 | })
49 |
50 | // the type of the successful response
51 | case class Foo(foo: String)
52 |
53 | // the client
54 | val client = new featherbed.Client(new URL("http://localhost:8768/api/"))
55 | ```
56 |
57 | When using the `send[T]` method, the resulting `Future` will *fail* if the server returns an HTTP error. This means that
58 | in order to handle an error, you must handle it at the `Future` level using the `Future` API:
59 |
60 | ```tut:book:nofail
61 | val req = client.get("not/found").accept("application/json")
62 |
63 | Await.result {
64 | req.send[Foo]().handle {
65 | case ErrorResponse(request, response) =>
66 | throw new Exception(s"Error response $response to request $request")
67 | }
68 | }
69 | ```
70 |
71 | This isn't a very useful error handler, but it demonstrates how errors can be intercepted at the `Future` level. The
72 | `handle` or `rescue` methods of `Future` can be used to recover from the failure. See their API docs for more
73 | information. The exception that's returned in a `Future` which failed due to a server error response is of type
74 | `ErrorResponse`, which contains the request and response.
75 |
76 | Often, the a REST API will be set up to return some meaningful representation of errors in the same content type as its
77 | responses. In the example above, our dummy server is set up to return JSON errors in a well-defined structure. To
78 | capture this, we can use the `send[Error, Success]` method instead of `send[T]`:
79 |
80 | ```tut:book
81 | // ADT for errors
82 | case class Error(error: String)
83 |
84 | val req = client.get("not/found").accept("application/json")
85 |
86 | Await.result(req.send[Error, Foo])
87 | ```
88 |
89 | Instead of an exception, we're capturing the server errors in an `Either[Error, Foo]`. This is a typical pattern in Scala functional
90 | programming for dealing with operations which may fail. The benefit is that the well-defined error type is also automatically
91 | decoded for us. However, if the error can't be decoded, this will still result in a failed `Future`, which fails on the
92 | decoding rather than the server error.
93 |
94 | Next, read about [Building REST Clients](06-building-rest-clients.html)
95 |
--------------------------------------------------------------------------------
/docs/src/main/tut/06-building-rest-clients.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Building REST Clients
3 | layout: default
4 | ---
5 |
6 | # Building REST Clients
7 |
8 | Previously, we looked at how featherbed's request specifications are evaluated lazily, and can be re-used to generate
9 | multiple requests of the same specification. In this segment, we'll see how this functionality can be leveraged
10 | to build resource-oriented client APIs for a REST service.
11 |
12 | As an example, we'll build a Scala client for a subset of the [JSONPlaceholder](http://jsonplaceholder.typicode.com/) API.
13 | The JSONPlaceholder defines the following data types:
14 |
15 | ```tut:book
16 | case class Post(userId: Int, id: Int, title: String, body: String)
17 |
18 | case class Comment(postId: Int, id: Int, name: String, email: String, body: String)
19 | ```
20 |
21 | We need the usual imports:
22 |
23 | ```tut:book
24 | import featherbed.circe._
25 | import io.circe.generic.auto._
26 | import shapeless.Coproduct
27 | import com.twitter.util.Await
28 | import java.net.URL
29 | ```
30 |
31 | And we can define a class for our API client:
32 |
33 | ```tut:book
34 | class JSONPlaceholderAPI(baseUrl: URL) {
35 | import featherbed.circe._
36 | import io.circe.generic.auto._
37 |
38 | private val client = new featherbed.Client(baseUrl)
39 |
40 | object posts {
41 |
42 | private val listRequest = client.get("posts").accept("application/json")
43 | private val getRequest = (id: Int) => client.get(s"posts/$id").accept("application/json")
44 |
45 | def list() = listRequest.send[Seq[Post]]()
46 | def get(id: Int) = getRequest(id).send[Post]()
47 |
48 | }
49 |
50 | object comments {
51 | private val listRequest = client.get("comments").accept("application/json")
52 | private val getRequest = (id: Int) => client.get(s"comments/$id").accept("application/json")
53 |
54 | def list() = listRequest.send[Seq[Comment]]()
55 | def get(id: Int) = getRequest(id).send[Comment]()
56 | }
57 | }
58 |
59 | val apiClient = new JSONPlaceholderAPI(new URL("http://jsonplaceholder.typicode.com/"))
60 |
61 | Await.result(apiClient.posts.list())
62 |
63 | Await.result(apiClient.posts.get(1))
64 |
65 |
66 | ```
67 |
--------------------------------------------------------------------------------
/docs/src/main/tut/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tutorial Index
3 | layout: default
4 | ---
5 |
6 | # Tutorial Index
7 |
8 | These tutorials will walk you through uses of featherbed. Because these tutorials are actually
9 | evaluated by [tut](https://github.com/tpolecat/tut) as part of the build process, they also
10 | function as unit tests!
11 |
12 | 1. [Installation](01-installation.html)
13 | 2. [Basic Usage](02-basic-usage.html)
14 | 3. [Content Types and Encoders](03-content-types-and-encoders.html)
15 | 4. [Response Decoding and Validation](04-response-decoding-and-validation.html)
16 | 5. [Error Handling](05-error-handling.html)
17 | 6. [Building REST Clients](06-building-rest-clients.html)
18 |
--------------------------------------------------------------------------------
/docs/src/site/_config.yml:
--------------------------------------------------------------------------------
1 | # Setup
2 | title: featherbed
3 | tagline: Asynchronous Scala HTTP client using Finagle, Shapeless and Cats
4 | baseurl: "/featherbed"
5 | #paginate: 1
6 |
7 | # About/contact
8 | author:
9 | name: Jeremy Smith
10 | url: http://jeremyrsmith.github.io
11 |
12 | # Gems
13 | gems:
14 | - jekyll-paginate
15 | - kramdown
16 | - rouge
17 |
18 | #Others
19 | markdown: kramdown
20 |
21 | kramdown:
22 | input: GFM
23 | syntax_highlighter: rouge
24 | highlighter: rouge
25 |
--------------------------------------------------------------------------------
/docs/src/site/_includes/head.html:
--------------------------------------------------------------------------------
1 |