diff --git a/.gitignore b/.gitignore index 2b78baf..e4f9ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ pom.xml.asc .lein-plugins/ .lein-failures doc/codox +/.idea +*iml diff --git a/README.md b/README.md index 0f364cb..70bcbe7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ If not, check out its [docs](https://twitter.github.io/finagle/guide/). ## Building - lein sub -s "lein-finagle-clojure:finagle-clojure-template:core:thrift" install + lein sub -s "lein-finagle-clojure:finagle-clojure-template:core:thrift:http" install ## Running Tests @@ -21,6 +21,7 @@ The readmes in each sub-library have more information. * `core`: convenience fns for interacting with Futures. * `thrift`: create Thrift clients & servers. +* `http`: create HTTP servers, requests, and responses * `lein-finagle-clojure`: a lein plugin for automatically compiling Thrift definitions using [Scrooge](https://twitter.github.io/scrooge/index.html). * `finagle-clojure-template`: a lein template for creating new projects using finagle-clojure & Thrift. diff --git a/core/README.md b/core/README.md index 61149df..9be7b21 100644 --- a/core/README.md +++ b/core/README.md @@ -15,3 +15,4 @@ This module contains wrappers for `com.twitter.util.Future` & core Finagle class * `finagle-clojure.futures`: wrappers around `Future` operations. * `finagle-clojure.scala`: sugar for Clojure/Scala interop. * `finagle-clojure.service`: wrappers for operations on `Service`. +* `finagle-clojure.server`: wrappers for creating, starting, and stopping `Server`s. diff --git a/core/src/finagle_clojure/builder/client.clj b/core/src/finagle_clojure/builder/client.clj new file mode 100644 index 0000000..3426c18 --- /dev/null +++ b/core/src/finagle_clojure/builder/client.clj @@ -0,0 +1,154 @@ +(ns finagle-clojure.builder.client + "Functions for creating and altering `com.twitter.finagle.Client` objects independent + of any particular codec. Generally speaking codec-specific client functions + should be preferred, but these are included for comptability with older systems + configured at the client level." + (:import (com.twitter.finagle.builder ClientBuilder) + (com.twitter.util Duration Future) + (com.twitter.finagle.stats StatsReceiver) + (java.util.logging Logger) + (com.twitter.finagle Service Client))) + +(defn ^ClientBuilder builder [] + "A builder for constructing `com.twitter.finagle.Client`s. Repeated changes to the builder should be + chained, as so: + + ``` + (-> (builder) + (named \"servicename\") + (bind-to 3000) + (build some-service)) + ``` + + *Arguments*: + + * None. + + *Returns*: + + a new instance of [[com.twitter.finagle.builder.ClientBuilder]]." + (ClientBuilder/get)) + +(defn ^Service build + "Given a completed `ClientBuilder`, return a new `Service` that represents this client. + + *Arguments*: + + * a ClientBuilder + + *Returns*: + + a [[com.twitter.finagle.Service]] that represents a client request" + [^ClientBuilder b] + (.unsafeBuild b)) + +(defn ^ClientBuilder codec + "Configures the given ServerBuilder with a codec. + + *Arguments*: + + * `b`: a ClientBuilder + * `cdc`: a Codec, CodecFactory, or Function1 that defines the server codec + + *Returns*: + + a ClientBuilder configured with the given codec" + [^ClientBuilder b cdc] + (.codec b cdc)) + +(defn ^ClientBuilder hosts + "Configures the given ClientBuilder with one or more hosts. + + *Arguments*: + + * `b`: a ClientBuilder + * `hosts`: a `SocketAddress`, `Seq` or comma-separated string of hostnames + + *Returns*: + + a ClientBuilder configured with the given hosts" + [^ClientBuilder b hosts] + (.hosts b hosts)) + +(defn ^ClientBuilder host-connection-limit + "Configures the given ClientBuilder with a host connection limit. + + *Arguments*: + + * `b`: a ClientBuilder + * `limit`: the number to limit connections to + + *Returns*: + + a ClientBuilder configured with the given limit" + [^ClientBuilder b limit] + (.hostConnectionLimit b (int limit))) + +(defn ^ClientBuilder tcp-connect-timeout + "Configures the given ClientBuilder with a TCP connection timeout. + + *Arguments*: + + * `b`: a ClientBuilder + * `timeout`: a [[com.twitter.util.Duration]] + + *Returns*: + + a ClientBuilder configured with the given timeout" + [^ClientBuilder b ^Duration timeout] + (.tcpConnectTimeout b timeout)) + +(defn ^ClientBuilder retries + "Configures the given ClientBuilder with a retry limit. + + *Arguments*: + + * `b`: a ClientBuilder + * `retries`: the number of times to retry + + *Returns*: + + a ClientBuilder configured with the given retries" + [^ClientBuilder b retries] + (.retries b (int retries))) + +(defn ^ClientBuilder report-to + "Configures the given ClientBuilder with a StatsReceiver to report to. + + *Arguments*: + + * `b`: a ClientBuilder + * `rcvr`: a [[com.twitter.finagle.stats.StatsReceiver]] + + *Returns*: + + a ClientBulider configured with the given StatsReceiver" + [^ClientBuilder b ^StatsReceiver rcvr] + (.reportTo b rcvr)) + +(defn ^ClientBuilder logger + "Configures the given ClientBuilder with a Logger. + + *Arguments*: + + * `b`: a ClientBuilder + * `l`: a [[java.util.logging.Logger]] + + *Returns* + + a ClientBuilder configured with the given Logger" + [^ClientBuilder b ^Logger l] + (.logger b l)) + +(defn ^Future close! + "Stops the given client. + + *Arguments*: + + * `client`: an instance of [[com.twitter.finagle.Client]] + + *Returns*: + + a Future that closes when the client stops" + [^Client client] + (.close client)) diff --git a/core/src/finagle_clojure/builder/server.clj b/core/src/finagle_clojure/builder/server.clj new file mode 100644 index 0000000..18e1af4 --- /dev/null +++ b/core/src/finagle_clojure/builder/server.clj @@ -0,0 +1,160 @@ +(ns finagle-clojure.builder.server + "Functions for creating and altering `com.twitter.finagle.Server` objects independent + of any particular codec. Generally speaking codec-specific server functions + should be preferred, but these are included for comptability with older systems + configured at the server level." + (:import (com.twitter.finagle.builder ServerBuilder Server) + (com.twitter.finagle Service) + (java.net InetSocketAddress) + (com.twitter.util Duration Future) + (com.twitter.finagle.tracing Tracer) + (com.twitter.finagle.stats StatsReceiver))) + +(defn ^ServerBuilder builder + "A handy Builder for constructing Servers (i.e., binding Services to a port). + The `ServerBuilder` requires the definition of `codec`, `bind-to` and `named`. + + The main class to use is [[com.twitter.finagle.builder.ServerBuilder]], as so: + + ``` + (-> (builder) + (named \"servicename\") + (bind-to 3000) + (build some-service)) + ``` + + *Arguments*: + + * None. + + *Returns*: + + a new instance of [[com.twitter.finagle.builder.ServerBuilder]]." + [] + (ServerBuilder/apply)) + +(defn ^Server build + "Given a completed `ServerBuilder` and a `Service`, constructs an `Server` which is capable of + responding to requests. + + *Arguments*: + + * `b`: a ServerBuilder + * `svc`: the Service this server will use to respond to requests + + *Returns*: + + a running instance of [[com.twitter.finagle.builder.Server]]" + [^ServerBuilder b ^Service svc] + (.unsafeBuild b svc)) + +(defn ^Future close! + "Stops the given Server. + + *Arguments*: + + * `server`: an instance of [[com.twitter.finagle.builder.Server]] + + *Returns*: + + a Future that closes when the server stops" + [^Server server] + (.close server)) + +(defn ^ServerBuilder named + "Configures the given ServerBuilder with a name. + + *Arguments*: + + * `b`: a ServerBuilder + * `name`: the name of this server + + *Returns*: + + a named ServerBuilder" + [^ServerBuilder b ^String name] + (.name b name)) + +(defn ^ServerBuilder bind-to + "Configures the given ServerBuilder with a port. + + *Arguments*: + + * `b`: a ServerBuilder + * `p`: the port number to bind this server to + + *Returns*: + + a bound ServerBuilder" + [^ServerBuilder b p] + (.bindTo b (InetSocketAddress. (int p)))) + +(defn ^ServerBuilder request-timeout + "Configures the given ServerBuilder with a request timeout. + + *Arguments*: + + * `b`: a ServerBuilder + * `d`: the duration of the request timeout for this server + + *Returns*: + + a ServerBuilder configured with the given timeout" + [^ServerBuilder b ^Duration d] + (.requestTimeout b d)) + +(defn ^ServerBuilder codec + "Configures the given ServerBuilder with a codec. + + *Arguments*: + + * `b`: a ServerBuilder + * `cdc`: a Codec, CodecFactory, or Function1 that defines the server codec + + *Returns*: + + a ServerBuilder configured with the given codec" + [^ServerBuilder b cdc] + (.codec b cdc)) + +(defn ^ServerBuilder max-concurrent-requests + "Configures the given ServerBuilder to accept a maximum number of concurrent requests. + + *Arguments*: + + * `b`: a ServerBuilder + * `mcr`: the maximum number of concurrent requests + + *Returns*: + + a ServerBuilder configured with a maximum number of concurrent requests" + [^ServerBuilder b mcr] + (.maxConcurrentRequests b (int mcr))) + +(defn ^ServerBuilder tracer + "Configures the given ServerBuilder to use a Tracer. + + *Arguments*: + + * `b`: a ServerBuilder + * `tracer`: a Tracer + + *Returns*: + + a ServerBuilder configured with the given tracer" + [^ServerBuilder b ^Tracer tracer] + (.tracer b tracer)) + +(defn ^ServerBuilder report-to + "Configures the given ServerBuilder to report to a stats receiver. + + *Arguments*: + + * `b`: a ServerBuilder + * `rcvr`: a StatsReceiver + + *Returns*: + + a ServerBuilder configured with the given stats receiver" + [^ServerBuilder b ^StatsReceiver rcvr] + (.reportTo b rcvr)) diff --git a/core/src/finagle_clojure/options.clj b/core/src/finagle_clojure/options.clj new file mode 100644 index 0000000..b34d425 --- /dev/null +++ b/core/src/finagle_clojure/options.clj @@ -0,0 +1,48 @@ +(ns finagle-clojure.options + "Functions for working with `scala.Option` objects." + (:import (scala Option)) + (:refer-clojure :exclude [get empty?])) + +(defn ^Option option + "Returns an Option with the given value `v`. + + *Arguments*: + + * `v`: the value that the new Option should be defined with. + + *Returns*: + + `Some(v)` if `v` is present and non-null, `None` otherwise" + ([] + (Option/empty)) + ([v] + (Option/apply v))) + +(defn empty? + "Does Option `o` have a value? Returns true if so, false otherwise. + + *Arguments*: + + * `o`: an Option + + *Returns*: + + true if `v` is None, false otherwise" + [^Option o] + (.isEmpty o)) + +(defn get + "Returns the value wrapped by `o`. + Although the Scala implementation throws a `Predef.NoSuchElementException` if called + on an empty Option, Clojure generally avoids throwing on empty gets, instead preferring to return nil. + This function adopts the Clojure behavior, choosing to treat this effectively as a call to `getOrNull`. + + *Arguments*: + + * `o`: an Option + + *Returns*: + + the Option's value if non-empty, nil otherwise" + [^Option o] + (when-not (empty? o) (.get o))) diff --git a/core/src/finagle_clojure/scala.clj b/core/src/finagle_clojure/scala.clj index 251feef..83cad68 100644 --- a/core/src/finagle_clojure/scala.clj +++ b/core/src/finagle_clojure/scala.clj @@ -2,7 +2,9 @@ "Utilities for interop with JVM classes generated from Scala code. Scala functions & methods expect Scala collection & function instances, not Java Collections or Clojure IFns." - (:import [scala.collection JavaConversions])) + (:import [scala.collection JavaConversions] + (scala Product) + (scala.runtime BoxedUnit))) ;; TODO: @samn: 06/11/14 add more wrappers for JavaConversions @@ -32,6 +34,25 @@ [scala-seq] (into [] (JavaConversions/seqAsJavaList scala-seq))) +(defn tuple->vec [^Product p] + "Convert a Scala Tuple to a vector. + + *Arguments*: + + * `p`: a Scala Product, generally a tuple + + *Returns*: + + A PersistentVector with the conents of `p`." + (->> (.productArity p) + (range) + (map #(.productElement p %)) + (into []))) + +(def unit + "The Scala Unit value." + BoxedUnit/UNIT) + (defn ^com.twitter.util.Function Function* ([apply-fn] (Function* apply-fn nil)) ([apply-fn defined-at-class] diff --git a/core/test/finagle_clojure/builder/client_test.clj b/core/test/finagle_clojure/builder/client_test.clj new file mode 100644 index 0000000..9126a13 --- /dev/null +++ b/core/test/finagle_clojure/builder/client_test.clj @@ -0,0 +1,33 @@ +(ns finagle-clojure.builder.client-test + (:import (com.twitter.finagle.builder ClientBuilder IncompleteSpecification) + (com.twitter.finagle Service)) + (:require [midje.sweet :refer :all] + [finagle-clojure.builder.client :refer :all] + [finagle-clojure.futures :as f] + [finagle-clojure.scala :as scala])) + +(facts "builder" + (-> + (builder) + (class)) + => ClientBuilder + + (-> (builder) + (build)) + => (throws IncompleteSpecification) + + (-> (builder) + (class)) + => ClientBuilder + + (-> (builder) + (build)) + => (throws IncompleteSpecification) + + (let [s (-> (builder) + (hosts "localhost:3000") + (build))] + (ancestors (class s)) + => (contains Service) + (f/await (close! s)) + => scala/unit)) diff --git a/core/test/finagle_clojure/builder/server_test.clj b/core/test/finagle_clojure/builder/server_test.clj new file mode 100644 index 0000000..0d09501 --- /dev/null +++ b/core/test/finagle_clojure/builder/server_test.clj @@ -0,0 +1,47 @@ +(ns finagle-clojure.builder.server-test + (:import (com.twitter.finagle.builder Server ServerBuilder IncompleteSpecification)) + (:require [midje.sweet :refer :all] + [finagle-clojure.builder.server :refer :all] + [finagle-clojure.service :as service] + [finagle-clojure.futures :as f] + [finagle-clojure.scala :as scala])) + +(def empty-service + (service/mk [req] + (f/value nil))) + +(facts "builder" + (-> + (builder) + (class)) + => ServerBuilder + + (-> (builder) + (build nil)) + => (throws IncompleteSpecification) + + (-> (builder) + (bind-to 3000) + (class)) + => ServerBuilder + + (-> (builder) + (bind-to 3000) + (build empty-service)) + => (throws IncompleteSpecification) + + (let [s (-> (builder) + (bind-to 3000) + (named "foo") + (build empty-service))] + (ancestors (class s)) + => (contains Server) + (f/await (close! s)) + => scala/unit) + + (-> (builder) + (bind-to 3000) + (named "foo") + (build empty-service) + (close!) + (f/await)) => scala/unit) diff --git a/core/test/finagle_clojure/options_test.clj b/core/test/finagle_clojure/options_test.clj new file mode 100644 index 0000000..79e1899 --- /dev/null +++ b/core/test/finagle_clojure/options_test.clj @@ -0,0 +1,21 @@ +(ns finagle-clojure.options-test + (:refer-clojure :exclude [get empty?]) + (:import (scala Some None$)) + (:require [midje.sweet :refer :all] + [finagle-clojure.options :refer :all])) + +(set! *warn-on-reflection* true) + +(facts "option creation" + (class (option)) => None$ + (class (option nil)) => None$ + (class (option :foo)) => Some + + (empty? (option)) => true + (empty? (option nil)) => true + (empty? (option :foo)) => false + + (get (option)) => nil + (get (option nil)) => nil + (get (option :foo)) => :foo + ) diff --git a/http/.gitignore b/http/.gitignore new file mode 100644 index 0000000..e04714b --- /dev/null +++ b/http/.gitignore @@ -0,0 +1,9 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port diff --git a/http/README.md b/http/README.md new file mode 100644 index 0000000..6354d59 --- /dev/null +++ b/http/README.md @@ -0,0 +1,12 @@ +# http + +This module contains wrappers for `com.twitter.finagle.HttpServer` and HTTP messages. + +### Dependency + + [finagle-clojure/http "0.1.2"] + +### Namespaces + +* `finagle-clojure.http.server`: a helper for building an HTTP server and a Clojured reference to the `Http` codec +* `finagle-clojure.http.message`: wrappers for creating `Response` and `Request` objects diff --git a/http/project.clj b/http/project.clj new file mode 100644 index 0000000..5b1f88c --- /dev/null +++ b/http/project.clj @@ -0,0 +1,13 @@ +(defproject finagle-clojure/http "0.1.2-SNAPSHOT" + :description "A light wrapper around Finagle HTTP for Clojure" + :url "https://github.com/twitter/finagle-clojure" + :license {:name "Apache License, Version 2.0" + :url "https://www.apache.org/licenses/LICENSE-2.0"} + :scm {:name "git" :url "http://github.com/finagle/finagle-clojure"} + :plugins [[lein-midje "3.1.3"]] + :profiles {:test {:dependencies [[midje "1.6.3" :exclusions [org.clojure/clojure]]]} + :dev [:test {:dependencies [[org.clojure/clojure "1.6.0"]]}] + :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} + :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}} + :dependencies [[finagle-clojure/core "0.1.2-SNAPSHOT"] + [com.twitter/finagle-http_2.10 "6.24.0"]]) diff --git a/http/src/finagle_clojure/http/builder/codec.clj b/http/src/finagle_clojure/http/builder/codec.clj new file mode 100644 index 0000000..291c155 --- /dev/null +++ b/http/src/finagle_clojure/http/builder/codec.clj @@ -0,0 +1,6 @@ +(ns finagle-clojure.http.builder.codec + (:import (com.twitter.finagle.http Http))) + +(def http + "The HTTP codec, for use with Finagle client and server builders." + (Http/get)) diff --git a/http/src/finagle_clojure/http/client.clj b/http/src/finagle_clojure/http/client.clj new file mode 100644 index 0000000..33667d4 --- /dev/null +++ b/http/src/finagle_clojure/http/client.clj @@ -0,0 +1,119 @@ +(ns finagle-clojure.http.client + (:import (com.twitter.finagle Http Http$Client) + (com.twitter.finagle Stack$Param Service) + (com.twitter.util StorageUnit Future))) + +(defn- ^Stack$Param param [p] + (reify Stack$Param (default [this] p))) + +(defn ^Http$Client with-tls + "Configures the given `Http.Client` with TLS. + + *Arguments*: + + * `client`: an Http.Client + * `cfg-or-hostname`: a `Netty3TransporterTLSConfig` config or hostname string + + *Returns*: + + the given `Http.Client`" + [^Http$Client client cfg-or-hostname] + (.withTls client cfg-or-hostname)) + +(defn ^Http$Client with-tls-without-validation + "Configures the given `Http.Client` with TLS without validation. + + *Arguments*: + + * `client`: an Http.Client + + *Returns*: + + the given `Http.Client`" + [^Http$Client client] + (.withTlsWithoutValidation client)) + +(defn ^Http$Client with-max-request-size + "Configures the given `Http.Client` with a max request size. + + *Arguments*: + + * `client`: an Http.Client + * `size`: a `StorageUnit` of the desired request size + + *Returns*: + + the given `Http.Client`" + [^Http$Client client ^StorageUnit size] + (.withMaxRequestSize client size)) + +(defn ^Http$Client with-max-response-size + "Configures the given `Http.Client` with a max response size. + + *Arguments*: + + * `client`: an Http.Client + * `size`: a `StorageUnit` of the desired response size + + *Returns*: + + the given `Http.Client`" + [^Http$Client client ^StorageUnit size] + (.withMaxResponseSize client size)) + +(defn ^Http$Client configured + "Configures the given `Http.Client` with the desired Stack.Param. Generally, prefer one of the + explicit configuration functions over this. + + *Arguments*: + + * `client`: an Http.Client + * `p`: a parameter that will be subsequently wrapped with `Stack.Param` + + *Returns*: + + the given `Http.Client`" + [^Http$Client client p] + (.configured client p (param p))) + +(defn ^Http$Client http-client + "The base HTTP client. Call `service` on this once configured to convert it to a full-fledged service. + + *Arguments*: + + * None. + + *Returns*: + + an instance of `Http.Client`" + [] + (Http/client)) + +(defn ^Service service + "Creates a new HTTP client structured as a Finagle `Service`. + + *Arguments*: + + * `dest`: a comma-separated string of one or more destinations with the form `\"hostname:port\"` + * `client` (optional): a preconfigured `Http.Client` + + *Returns*: + + a Finagle `Service`" + ([dest] + (service (http-client) dest)) + ([^Http$Client client dest] + (.newService client dest))) + +(defn ^Future close! + "Stops the given client. + + *Arguments*: + + * `client`: an instance of [[com.twitter.finagle.Client]] + + *Returns*: + + a Future that closes when the client stops" + [^Http$Client client] + (.close client)) diff --git a/http/src/finagle_clojure/http/message.clj b/http/src/finagle_clojure/http/message.clj new file mode 100644 index 0000000..306caef --- /dev/null +++ b/http/src/finagle_clojure/http/message.clj @@ -0,0 +1,163 @@ +(ns finagle-clojure.http.message + "Functions for working with [[com.twitter.finagle.http.Message]] and its concrete subclasses, + [[com.twitter.finagle.http.Request]] and [[com.twitter.finagle.http.Response]]. + + `Request` objects are passed to services bound to a Finagle HTTP server, and `Response` objects must be passed back + (wrapped in a `Future`) in turn. Most requests are constructed by Finagle, but the functions here to may be helpful + to create `MockRequest`s for service testing purposes." + (:import (com.twitter.finagle.http Response Request Message) + (org.jboss.netty.handler.codec.http HttpResponseStatus HttpMethod)) + (:require [finagle-clojure.options :as opt])) + +(defn- ^HttpResponseStatus int->HttpResponseStatus [c] + (HttpResponseStatus/valueOf (int c))) + +(defn- ^HttpMethod str->HttpMethod [m] + (HttpMethod/valueOf (-> m (name) (.toUpperCase)))) + +(defn ^Response response + "Constructs a `Response`, required for Finagle services that interact with an `HttpServer`. + + *Arguments*: + + * `code` (optional): a number representing the desired HTTP status code of the response + + *Returns*: + + an instance of [[com.twitter.finagle.http.Response]]" + ([] + (Response/apply)) + ([code] + (Response/apply (int->HttpResponseStatus code)))) + +(defn ^Request request + "Constructs a `Request`. Usually this will be constructed on your behalf for incoming requests; + this function is useful primarily testing purposes, and indeed returns a `MockRequest` in its current form. + + *Arguments*: + + * `uri`: the URI of the request + * `method` (optional): a keyword or string of the desired HTTP method + + *Returns*: + + an instance of [[com.twitter.finagle.http.Request]], specifically a MockRequest" + ([uri] + (Request/apply uri)) + ([uri method] + (Request/apply (str->HttpMethod method) uri))) + +(defn ^Response set-status-code + "Sets the status code of the given response. + + *Arguments*: + + * `resp`: a [[com.twitter.finagle.http.Response]] + * `code`: a number with the desired HTTP status code + + *Returns*: + + the given response" + [^Response resp code] + (.setStatusCode resp code) + resp) + +(defn status-code + "Returns the status code of the given `Response`. + + *Arguments*: + + * `resp`: a [[com.twitter.finagle.http.Response]] + + *Returns*: + + the status code of the response as an int" + [^Response resp] + (.statusCode resp)) + +(defn ^Message set-content-string + "Sets the content string of the given message. + + *Arguments*: + + * `msg`: a [[com.twitter.finagle.http.Message]] + * `content`: a string of content + + *Returns*: + + the given message" + [^Message msg content] + (.setContentString msg content) + msg) + +(defn ^String content-string + "Gets the content string of the given message. + + *Arguments*: + + * `msg`: a [[com.twitter.finagle.http.Message]] + + *Returns*: + + the content string of the message" + [^Message msg] + (.contentString msg)) + +(defn ^Message set-content-type + "Sets the content type of the given message. + + *Arguments*: + + * `msg`: a [[com.twitter.finagle.http.Message]] + * `type`: a string containing the message's content-type + * `charset` (optional, default: `utf-8`): the charset of the content + + *Returns*: + + the given message" + ([^Message msg type] + (set-content-type msg type "utf-8")) + ([^Message msg type charset] + (.setContentType msg type charset) + msg)) + +(defn ^String content-type + "Gets the content type of the given message. + + *Arguments*: + + * `msg`: a [[com.twitter.finagle.http.Message]] + + *Returns*: + + the content type of the message" + [^Message msg] + (opt/get (.contentType msg))) + +(defn ^Request set-http-method + "Sets the HTTP method of the given request. + + *Arguments*: + + * `req`: a [[com.twitter.finagle.http.Request]] + * `meth`: a string or keyword containing a valid HTTP method + + *Returns*: + + the given request" + [^Request req meth] + (.setMethod req (str->HttpMethod meth)) + req) + +(defn http-method + "Gets the HTTP method of the given request. + + *Arguments*: + + * `req`: a [[com.twitter.finagle.http.Request]] + + *Returns*: + + the HTTP method of the request as an uppercase string" + [^Request req] + (-> req (.method) (.getName))) \ No newline at end of file diff --git a/http/src/finagle_clojure/http/server.clj b/http/src/finagle_clojure/http/server.clj new file mode 100644 index 0000000..10455f6 --- /dev/null +++ b/http/src/finagle_clojure/http/server.clj @@ -0,0 +1,110 @@ +(ns finagle-clojure.http.server + (:import (com.twitter.finagle Http Http$Server) + (com.twitter.finagle Stack$Param ListeningServer) + (com.twitter.finagle.netty3 Netty3ListenerTLSConfig) + (com.twitter.util StorageUnit Future))) + +(defn- ^Stack$Param param [p] + (reify Stack$Param (default [this] p))) + +(defn ^Http$Server with-tls + "Configures the given `Http.Server` with TLS. + + *Arguments*: + + * `server`: an Http.Server + * `cfg`: a `Netty3ListenerTLSConfig` config + + *Returns*: + + the given `Http.Server`" + [^Http$Server server ^Netty3ListenerTLSConfig cfg] + (.withTls server cfg)) + +(defn ^Http$Server with-max-request-size + "Configures the given `Http.Server` with a max request size. + + *Arguments*: + + * `server`: an Http.Server + * `size`: a `StorageUnit` of the desired request size + + *Returns*: + + the given `Http.Server`" + [^Http$Server server ^StorageUnit size] + (.withMaxRequestSize server size)) + +(defn ^Http$Server with-max-response-size + "Configures the given `Http.Server` with a max response size. + + *Arguments*: + + * `server`: an Http.Server + * `size`: a `StorageUnit` of the desired response size + + *Returns*: + + the given `Http.Server`" + [^Http$Server server ^StorageUnit size] + (.withMaxResponseSize server size)) + +(defn ^Http$Server configured + "Configures the given `Http.Server` with the desired Stack.Param. Generally, prefer one of the + explicit configuration functions over this. + + *Arguments*: + + * `server`: an Http.Server + * `p`: a parameter that will be subsequently wrapped with `Stack.Param` + + *Returns*: + + the given `Http.Server`" + [^Http$Server server p] + (.configured server p (param p))) + +(defn ^Http$Server http-server + "The base HTTP server. Call `serve` on this once configured to begin listening to requests. + + *Arguments*: + + * None. + + *Returns*: + + an instance of `Http.Server`" + [] + (Http/server)) + +(defn ^ListeningServer serve + "Creates a new HTTP server listening on the given address and responding with the given service or + service factory. The service must accept requests of type `HttpRequest`, and respond with a Future + wrapping an `HttpResponse`. + + *Arguments*: + + * `address`: a listening address, either a string of the form `\":port\"` or a `SocketAddress` + * `service`: a responding service, either a `Service` or a `ServiceFactory` + * `server` (optional): a preconfigured `Http.Server` + + *Returns*: + + a running `ListeningServer`" + ([address service] + (serve (http-server) address service)) + ([^Http$Server server address service] + (.serve server address service))) + +(defn ^Future close! + "Stops the given Server. + + *Arguments*: + + * `server`: an Http.Server + + *Returns*: + + a Future that closes when the server stops" + [^Http$Server server] + (.close server)) diff --git a/http/test/finagle_clojure/http/client_test.clj b/http/test/finagle_clojure/http/client_test.clj new file mode 100644 index 0000000..7ed4094 --- /dev/null +++ b/http/test/finagle_clojure/http/client_test.clj @@ -0,0 +1,74 @@ +(ns finagle-clojure.http.client-test + (:import (com.twitter.finagle Http$Client Http$param$MaxRequestSize Http$param$MaxResponseSize) + (com.twitter.finagle.transport Transport$TLSClientEngine) + (com.twitter.util StorageUnit) + (com.twitter.finagle.client Transporter$TLSHostname)) + (:require [midje.sweet :refer :all] + [finagle-clojure.http.stack-helpers :refer :all] + [finagle-clojure.http.client :refer :all] + [finagle-clojure.options :as opt])) + +(defn- tls-hostname [^Http$Client client] + (when-let [p (extract-param client Transporter$TLSHostname)] + (.hostname p))) + +(defn- tls-client-engine [^Http$Client client] + (when-let [p (extract-param client Transport$TLSClientEngine)] + (.e p))) + +(defn- max-request-size [^Http$Client client] + (when-let [p (extract-param client Http$param$MaxRequestSize)] + (.size p))) + +(defn- max-response-size [^Http$Client client] + (when-let [p (extract-param client Http$param$MaxResponseSize)] + (.size p))) + +(facts "HTTP server" + (facts "during configuration" + (tls-hostname (http-client)) + => nil + + (tls-client-engine (http-client)) + => nil + + (-> (http-client) + (with-tls "example.com") + (tls-hostname) + (opt/get)) + => "example.com" + + (-> (http-client) + (with-tls "example.com") + (tls-client-engine) + (opt/get) + (class) + (ancestors)) + => (contains scala.Function1) + + (-> (http-client) + (with-tls-without-validation) + (tls-hostname)) + => nil + + (-> (http-client) + (with-tls-without-validation) + (tls-client-engine) + (opt/get) + (class) + (ancestors)) + => (contains scala.Function1) + + (max-request-size (http-client)) + => nil + + (max-request-size + (with-max-request-size (http-client) (StorageUnit. 1024))) + => (StorageUnit. 1024) + + (max-response-size (http-client)) + => nil + + (max-response-size + (with-max-response-size (http-client) (StorageUnit. 1024))) + => (StorageUnit. 1024))) \ No newline at end of file diff --git a/http/test/finagle_clojure/http/integration_test.clj b/http/test/finagle_clojure/http/integration_test.clj new file mode 100644 index 0000000..5565a52 --- /dev/null +++ b/http/test/finagle_clojure/http/integration_test.clj @@ -0,0 +1,62 @@ +(ns finagle-clojure.http.integration-test + (:import (com.twitter.finagle.http Request Response) + (com.twitter.finagle Service)) + (:require [midje.sweet :refer :all] + [finagle-clojure.scala :as scala] + [finagle-clojure.futures :as f] + [finagle-clojure.http.message :as m] + [finagle-clojure.service :as s] + [finagle-clojure.http.client :as http-client] + [finagle-clojure.http.server :as http-server] + [finagle-clojure.builder.client :as builder-client] + [finagle-clojure.builder.server :as builder-server] + [finagle-clojure.http.builder.codec :as http-codec] + )) + +(def ^Service hello-world + (s/mk [^Request req] + (f/value + (-> (m/response 200) + (m/set-content-string "Hello, World"))))) + +(facts "stack-based server and client" + (fact "performs a full-stack integration call" + (let [s (http-server/serve ":3000" hello-world) + c (http-client/service ":3000")] + (-> (s/apply c (m/request "/")) + (f/await) + (m/content-string)) + => "Hello, World" + + (f/await (http-client/close! c)) + => scala/unit + + (f/await (http-server/close! s)) + => scala/unit + ))) + +(facts "builder-based server and client" + (fact "performs a full-stack integration call" + (let [s (-> + (builder-server/builder) + (builder-server/codec http-codec/http) + (builder-server/bind-to 3000) + (builder-server/named "test") + (builder-server/build hello-world)) + c (-> + (builder-client/builder) + (builder-client/codec http-codec/http) + (builder-client/hosts "localhost:3000") + (builder-client/build))] + (-> (s/apply c (m/request "/")) + (f/map [rep] (Response/apply rep)) + (f/await) + (m/content-string)) + => "Hello, World" + + (f/await (builder-client/close! c)) + => scala/unit + + (f/await (builder-server/close! s)) + => scala/unit + ))) \ No newline at end of file diff --git a/http/test/finagle_clojure/http/message_test.clj b/http/test/finagle_clojure/http/message_test.clj new file mode 100644 index 0000000..d248b47 --- /dev/null +++ b/http/test/finagle_clojure/http/message_test.clj @@ -0,0 +1,35 @@ +(ns finagle-clojure.http.message-test + (:require [finagle-clojure.http.message :refer :all] + [midje.sweet :refer :all])) + +(fact "response status" + (-> (response) (set-status-code 200) (status-code)) + => 200 + + (-> (response 200) (status-code)) + => 200) + +(fact "request method" + (-> (request "/") (set-http-method :get) (http-method)) + => "GET" + + (-> (request "/" :get) (http-method)) + => "GET") + +(fact "content string" + (-> (response) (set-content-string "test") (content-string)) + => "test" + + (-> (request "/") (set-content-string "test") (content-string)) + => "test") + +(fact "content type" + (-> (response) (content-type)) + => nil + + (-> (response) (set-content-type "application/json") (content-type)) + => "application/json;charset=utf-8" + + (-> (response) (set-content-type "application/json" "us-ascii") (content-type)) + => "application/json;charset=us-ascii") + diff --git a/http/test/finagle_clojure/http/server_test.clj b/http/test/finagle_clojure/http/server_test.clj new file mode 100644 index 0000000..9bc56b1 --- /dev/null +++ b/http/test/finagle_clojure/http/server_test.clj @@ -0,0 +1,48 @@ +(ns finagle-clojure.http.server-test + (:import (com.twitter.finagle Http$Server Http$param$MaxRequestSize Http$param$MaxResponseSize) + (com.twitter.util StorageUnit) + (com.twitter.finagle.transport Transport$TLSServerEngine) + (com.twitter.finagle.netty3 Netty3ListenerTLSConfig)) + (:require [finagle-clojure.http.server :refer :all] + [finagle-clojure.http.stack-helpers :refer :all] + [finagle-clojure.options :as opt] + [midje.sweet :refer :all] + [finagle-clojure.scala :as scala])) + +(defn- tls-server-engine [^Http$Server server] + (when-let [p (extract-param server Transport$TLSServerEngine)] + (.e p))) + +(defn- max-request-size [^Http$Server server] + (when-let [p (extract-param server Http$param$MaxRequestSize)] + (.size p))) + +(defn- max-response-size [^Http$Server server] + (when-let [p (extract-param server Http$param$MaxResponseSize)] + (.size p))) + +(facts "HTTP server" + (facts "during configuration" + (tls-server-engine + (http-server)) => nil + + (-> (http-server) + (with-tls (Netty3ListenerTLSConfig. (scala/Function0 nil))) + (tls-server-engine) + (opt/get) + (.apply)) + => nil + + (max-request-size (http-server)) + => nil + + (max-request-size + (with-max-request-size (http-server) (StorageUnit. 1024))) + => (StorageUnit. 1024) + + (max-response-size (http-server)) + => nil + + (max-response-size + (with-max-response-size (http-server) (StorageUnit. 1024))) + => (StorageUnit. 1024))) \ No newline at end of file diff --git a/http/test/finagle_clojure/http/stack_helpers.clj b/http/test/finagle_clojure/http/stack_helpers.clj new file mode 100644 index 0000000..59b03b4 --- /dev/null +++ b/http/test/finagle_clojure/http/stack_helpers.clj @@ -0,0 +1,14 @@ +(ns finagle-clojure.http.stack-helpers + (:import (com.twitter.finagle Http$Server Stack$Parameterized) + (scala.collection JavaConversions)) + (:require [finagle-clojure.scala :as scala])) + +(defn- params [^Stack$Parameterized stackable] + (map scala/tuple->vec (JavaConversions/asJavaCollection (.params stackable)))) + +(defn extract-param [^Stack$Parameterized stackable ^Class cls] + (->> stackable + (params) + (flatten) + (filter #(instance? cls %)) + (first))) diff --git a/project.clj b/project.clj index bd2b935..ba3c836 100644 --- a/project.clj +++ b/project.clj @@ -6,12 +6,13 @@ :scm {:name "git" :url "http://github.com/finagle/finagle-clojure"} :dependencies [[finagle-clojure/core "0.1.2-SNAPSHOT"] [finagle-clojure/thrift "0.1.2-SNAPSHOT"] - [finagle-clojure/thriftmux "0.1.2-SNAPSHOT"]] + [finagle-clojure/thriftmux "0.1.2-SNAPSHOT"] + [finagle-clojure/http "0.1.2-SNAPSHOT"]] :plugins [[lein-sub "0.3.0"] [codox "0.8.10"] [lein-midje "3.1.3"]] - :sub ["core" "thrift"] - :codox {:sources ["core/src" "thrift/src" "thriftmux/src"] + :sub ["core" "thrift" "http"] + :codox {:sources ["core/src" "thrift/src" "thriftmux/src" "http/src"] :defaults {:doc/format :markdown} :output-dir "doc/codox" :src-dir-uri "https://github.com/finagle/finagle-clojure/blob/master/"