tags to code blocks
19 | code = code.sub(//,'')
20 | code = code.sub(/<\/pre>/,"
")
21 | end
22 |
23 | def strip_margin(text, spaces)
24 | lines = text.strip.split("\n")
25 | lines[0] << "\n" << lines[1..-1].map { |l| l[spaces..-1] }.join("\n")
26 | end
27 |
28 | def render(context)
29 | return "Code ref file '#{@file}' does not exist." unless File.exist?(@file)
30 |
31 | indented = (File.read(@file).match(/(?:\/\/\/|###)\s*code_ref\s*\:\s*#{@item}(.*?)(?:\/{3}|###)\s*end_code_ref/mi)||[])[1]
32 | spaces = indented[1..-1].match(/(\s*)[^ ]/)[1].size
33 | code = spaces == 0 ? indented : strip_margin(indented, spaces)
34 |
35 | return "No code matched the key #{@item} in #{@file}" unless code
36 |
37 | lexer = Pygments::Lexer.find_by_extname(File.extname(@file)).aliases[0]
38 | highlighted = Pygments.highlight(code, :lexer => lexer, :options => { :style => "default", :encoding => 'utf-8'})
39 | add_code_tags(highlighted, lexer)
40 | end
41 | end
42 |
43 | end
44 |
45 | Liquid::Template.register_tag('code_ref', Jekyll::CodeRefTag)
--------------------------------------------------------------------------------
/src/main/site/_plugins/generate_page_toc.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | class PageTocTag < Liquid::Tag
3 | def initialize(tag_name, args, tokens)
4 | @toc = Redcarpet::Markdown.new(Redcarpet::Render::HTML_TOC).render(tokens.join("\n"))
5 | end
6 | def render(context)
7 | @toc
8 | end
9 | end
10 |
11 | end
12 |
13 | Liquid::Template.register_tag('page_toc', Jekyll::PageTocTag)
--------------------------------------------------------------------------------
/src/main/site/_plugins/redcarpet2_markdown.rb:
--------------------------------------------------------------------------------
1 | require 'fileutils'
2 | require 'digest/md5'
3 | require 'redcarpet'
4 | require 'pygments'
5 |
6 | class Redcarpet2Markdown < Redcarpet::Render::HTML
7 | def block_code(code, lang)
8 | lang = lang || "text"
9 | colorized = Pygments.highlight(code, :lexer => lang, :options => { :style => "default", :encoding => 'utf-8'})
10 | add_code_tags(colorized, lang)
11 | end
12 |
13 | def add_code_tags(code, lang)
14 | code.sub(//, "").
15 | sub(/<\/pre>/, "
")
16 | end
17 | end
18 |
19 |
20 | class Jekyll::MarkdownConverter
21 | def extensions
22 | Hash[ *@config['redcarpet']['extensions'].map {|e| [e.to_sym, true] }.flatten ]
23 | end
24 |
25 | def markdown
26 | @markdown ||= Redcarpet::Markdown.new(Redcarpet2Markdown.new(extensions), extensions)
27 | end
28 |
29 | def convert(content)
30 | return super unless @config['markdown'] == 'redcarpet2'
31 | markdown.render(content)
32 | end
33 | end
--------------------------------------------------------------------------------
/src/main/site/_plugins/sass_converter.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | # Sass plugin to convert .scss to .css
3 | #
4 | # Note: This is configured to use the new css like syntax available in sass.
5 | require 'sass'
6 | require 'compass'
7 | class SassConverter < Converter
8 | safe true
9 | priority :low
10 |
11 | def matches(ext)
12 | ext =~ /scss/i
13 | end
14 |
15 | def output_ext(ext)
16 | ".css"
17 | end
18 |
19 | def convert(content)
20 | begin
21 | Compass.add_project_configuration
22 | Compass.configuration.project_path ||= Dir.pwd
23 |
24 | load_paths = [".", "./scss", "./css"]
25 | load_paths += Compass.configuration.sass_load_paths
26 |
27 | engine = Sass::Engine.new(content, :syntax => :scss, :load_paths => load_paths, :style => :compact)
28 | engine.render
29 | rescue StandardError => e
30 | puts "!!! SASS Error: " + e.message
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/src/main/site/_posts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/backchatio/hookup/2913794eb45d90d65713c9fd631b427abcca2d05/src/main/site/_posts/.gitkeep
--------------------------------------------------------------------------------
/src/main/site/config.rb:
--------------------------------------------------------------------------------
1 | # Require any additional compass plugins here.
2 |
3 | # Set this to the root of your project when deployed:
4 | http_path = "/"
5 | css_dir = "stylesheets"
6 | sass_dir = "stylesheets"
7 | images_dir = "imgs"
8 | javascripts_dir = "javascripts"
9 |
10 | # You can select your preferred output style here (can be overridden via the command line):
11 | # output_style = :expanded or :nested or :compact or :compressed
12 |
13 | # To enable relative paths to assets via compass helper functions. Uncomment:
14 | # relative_assets = true
15 |
16 | # To disable debugging comments that display the original location of your selectors. Uncomment:
17 | # line_comments = false
18 |
19 |
20 | # If you prefer the indented syntax, you might want to regenerate this
21 | # project again passing --syntax sass, or you can uncomment this:
22 | # preferred_syntax = :sass
23 | # and then run:
24 | # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
25 |
--------------------------------------------------------------------------------
/src/main/site/imgs/bg-watercolor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/backchatio/hookup/2913794eb45d90d65713c9fd631b427abcca2d05/src/main/site/imgs/bg-watercolor.jpg
--------------------------------------------------------------------------------
/src/main/site/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/backchatio/hookup/2913794eb45d90d65713c9fd631b427abcca2d05/src/main/site/imgs/logo.png
--------------------------------------------------------------------------------
/src/main/site/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: backchatio
3 | title: BackChat.io hookup
4 | ---
5 |
6 | # Reliable messaging on top of websockets.
7 |
8 | A scala based client and server for websockets based on netty and akka futures.
9 | It draws its inspiration from finagle, faye-websocket, zeromq, akka, ...
10 |
11 | The aim of this project is to provide a websocket client in multiple languages an to be used in non-browser applications.
12 | This client should be reliable by making a best effort not to lose any messages and gracefully recover from disconnections.
13 |
14 | The server should serve regular websocket applications but can be configured for more reliability too.
15 |
16 | ## Features
17 | To reach said goals this library implements:
18 |
19 | ### Protocol features:
20 |
21 | These features are baked into the default `JsonProtocolWireFormat` or in the WebSocket spec.
22 |
23 | #### Message Acking:
24 | You can decide if you want to ack a message on a per message basis.
25 |
26 | ```scala
27 | client ! "the message".needsAck(within = 5 seconds)
28 | ```
29 |
30 | #### PingPong
31 | This is baked into the websocket protocol, the library ensures it really happens
32 |
33 | ### Client only features:
34 |
35 | There are a number of extras baked into the client, of course they can be enabled and disabled based on config.
36 |
37 | #### Reconnection
38 |
39 | The client reconnects to the server on a backoff schedule indefinitely or for a maximum amount of times
40 |
41 | #### Message buffering
42 |
43 | During phases of disconnection it will buffer the messages to a file so that upon reconnection the messages will all be sent to the server.
44 |
45 | ## Usage
46 |
47 | This library is available on maven central.
48 |
49 | ```scala
50 | libraryDependencies += "io.backchat.hookup" %% "hookup" % "0.2.2"
51 | ```
52 |
53 | #### Create a websocket server
54 |
55 | ```scala
56 | import io.backchat.hookup._
57 |
58 | (HookupServer(8125) {
59 | new HookupServerClient {
60 | def receive = {
61 | case TextMessage(text) =>
62 | println(text)
63 | send(text)
64 | }
65 | }
66 | }).start
67 | ```
68 |
69 | #### Create a websocket client
70 |
71 | ```scala
72 | import io.backchat.hookup._
73 |
74 | new DefaultHookupClient(HookupClientConfig(new URI("ws://localhost:8080/thesocket"))) {
75 |
76 | def receive = {
77 | case Disconnected(_) ⇒
78 | println("The websocket to " + uri.toASCIIString + " disconnected.")
79 | case TextMessage(message) ⇒ {
80 | println("RECV: " + message)
81 | send("ECHO: " + message)
82 | }
83 | }
84 |
85 | connect() onSuccess {
86 | case Success ⇒
87 | println("The websocket is connected to:"+this.uri.toASCIIString+".")
88 | system.scheduler.schedule(0 seconds, 1 second) {
89 | send("message " + messageCounter.incrementAndGet().toString)
90 | }
91 | case _ ⇒
92 | }
93 | }
94 | ```
95 |
96 | There are [code examples](https://github.com/backchatio/hookup/tree/master/src/main/scala/io/backchat/hookup /examples) that show all the events being raised and a chat server/client.
97 |
98 | * Echo ([server](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PrintingEchoServer.scala) | [client](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PrintingEchoClient.scala))
99 | * All Events ([server](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PrintAllEventsServer.scala) | [client](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PrintAllEventsClient.scala))
100 | * Chat ([server](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/ChatServer.scala) | [client](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/ChatClient.scala))
101 | * PubSub ([server](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PubSubServer.scala) | [client](https://github.com/backchatio/hookup/blob/master/src/main/scala/io/backchat/hookup/examples/PubSubClient.scala))
102 |
103 | ## Patches
104 | Patches are gladly accepted from their original author. Along with any patches, please state that the patch is your original work and that you license the work to the *backchat-websocket* project under the MIT License.
105 |
106 | ## License
107 | MIT licensed. check the [LICENSE](https://github.com/backchatio/hookup/blob/master/LICENSE) file
--------------------------------------------------------------------------------
/src/main/site/javascripts/fixed-sidebar.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var sidebar = document.getElementById("sidebar");
3 |
4 | function getScrollTop(){
5 | if(typeof pageYOffset!= 'undefined'){
6 | return pageYOffset;//most browsers
7 | }
8 | else{
9 | var B= document.body; //IE 'quirks'
10 | var D= document.documentElement; //IE with doctype
11 | D= (D.clientHeight)? D: B;
12 | return D.scrollTop;
13 | }
14 | }
15 |
16 | function setSidebarPosition() {
17 | if(getScrollTop() > 350) {
18 | sidebar.style.top = "50px";
19 | sidebar.style.position = "fixed";
20 | } else {
21 | sidebar.style.top = "";
22 | sidebar.style.position = "absolute";
23 | }
24 | }
25 |
26 | window.onscroll = setSidebarPosition;
27 | })();
--------------------------------------------------------------------------------
/src/main/site/javascripts/scale.fix.js:
--------------------------------------------------------------------------------
1 | var metas = document.getElementsByTagName('meta');
2 | var i;
3 | if (navigator.userAgent.match(/iPhone/i)) {
4 | for (i=0; i
6 | *
7 | * */
8 |
--------------------------------------------------------------------------------
/src/main/site/stylesheets/print.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 | /* Welcome to Compass. Use this file to define print styles.
4 | * Import this file using the following HTML or equivalent:
5 | * */
6 |
--------------------------------------------------------------------------------
/src/main/site/stylesheets/pygment_github.css:
--------------------------------------------------------------------------------
1 | .highlight { background: #ffffff; }
2 | .highlight .c { color: #999988; font-style: italic } /* Comment */
3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
4 | .highlight .k { font-weight: bold } /* Keyword */
5 | .highlight .o { font-weight: bold } /* Operator */
6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
12 | .highlight .ge { font-style: italic } /* Generic.Emph */
13 | .highlight .gr { color: #aa0000 } /* Generic.Error */
14 | .highlight .gh { color: #999999 } /* Generic.Heading */
15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
17 | .highlight .go { color: #888888 } /* Generic.Output */
18 | .highlight .gp { color: #555555 } /* Generic.Prompt */
19 | .highlight .gs { font-weight: bold } /* Generic.Strong */
20 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */
21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */
22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */
23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */
24 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */
25 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */
26 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
27 | .highlight .m { color: #009999 } /* Literal.Number */
28 | .highlight .s { color: #d14 } /* Literal.String */
29 | .highlight .na { color: #008080 } /* Name.Attribute */
30 | .highlight .nb { color: #0086B3 } /* Name.Builtin */
31 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
32 | .highlight .no { color: #008080 } /* Name.Constant */
33 | .highlight .ni { color: #800080 } /* Name.Entity */
34 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
35 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
36 | .highlight .nn { color: #555555 } /* Name.Namespace */
37 | .highlight .nt { color: #000080 } /* Name.Tag */
38 | .highlight .nv { color: #008080 } /* Name.Variable */
39 | .highlight .ow { font-weight: bold } /* Operator.Word */
40 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */
41 | .highlight .mf { color: #009999 } /* Literal.Number.Float */
42 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */
43 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */
44 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */
45 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */
46 | .highlight .sc { color: #d14 } /* Literal.String.Char */
47 | .highlight .sd { color: #d14 } /* Literal.String.Doc */
48 | .highlight .s2 { color: #d14 } /* Literal.String.Double */
49 | .highlight .se { color: #d14 } /* Literal.String.Escape */
50 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */
51 | .highlight .si { color: #d14 } /* Literal.String.Interpol */
52 | .highlight .sx { color: #d14 } /* Literal.String.Other */
53 | .highlight .sr { color: #009926 } /* Literal.String.Regex */
54 | .highlight .s1 { color: #d14 } /* Literal.String.Single */
55 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */
56 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
57 | .highlight .vc { color: #008080 } /* Name.Variable.Class */
58 | .highlight .vg { color: #008080 } /* Name.Variable.Global */
59 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */
60 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
--------------------------------------------------------------------------------
/src/main/site/stylesheets/pygment_trac.css:
--------------------------------------------------------------------------------
1 | .highlight { background: #ffffff; }
2 | .highlight .c { color: #999988; font-style: italic } /* Comment */
3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
4 | .highlight .k { font-weight: bold } /* Keyword */
5 | .highlight .o { font-weight: bold } /* Operator */
6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
12 | .highlight .ge { font-style: italic } /* Generic.Emph */
13 | .highlight .gr { color: #aa0000 } /* Generic.Error */
14 | .highlight .gh { color: #999999 } /* Generic.Heading */
15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
17 | .highlight .go { color: #888888 } /* Generic.Output */
18 | .highlight .gp { color: #555555 } /* Generic.Prompt */
19 | .highlight .gs { font-weight: bold } /* Generic.Strong */
20 | .highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */
22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */
23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */
24 | .highlight .kn { font-weight: bold } /* Keyword.Namespace */
25 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */
26 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */
27 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
28 | .highlight .m { color: #009999 } /* Literal.Number */
29 | .highlight .s { color: #d14 } /* Literal.String */
30 | .highlight .na { color: #008080 } /* Name.Attribute */
31 | .highlight .nb { color: #0086B3 } /* Name.Builtin */
32 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
33 | .highlight .no { color: #008080 } /* Name.Constant */
34 | .highlight .ni { color: #800080 } /* Name.Entity */
35 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
36 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
37 | .highlight .nn { color: #555555 } /* Name.Namespace */
38 | .highlight .nt { color: #000080 } /* Name.Tag */
39 | .highlight .nv { color: #008080 } /* Name.Variable */
40 | .highlight .ow { font-weight: bold } /* Operator.Word */
41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */
42 | .highlight .mf { color: #009999 } /* Literal.Number.Float */
43 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */
44 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */
45 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */
46 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */
47 | .highlight .sc { color: #d14 } /* Literal.String.Char */
48 | .highlight .sd { color: #d14 } /* Literal.String.Doc */
49 | .highlight .s2 { color: #d14 } /* Literal.String.Double */
50 | .highlight .se { color: #d14 } /* Literal.String.Escape */
51 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */
52 | .highlight .si { color: #d14 } /* Literal.String.Interpol */
53 | .highlight .sx { color: #d14 } /* Literal.String.Other */
54 | .highlight .sr { color: #009926 } /* Literal.String.Regex */
55 | .highlight .s1 { color: #d14 } /* Literal.String.Single */
56 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */
57 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
58 | .highlight .vc { color: #008080 } /* Name.Variable.Class */
59 | .highlight .vg { color: #008080 } /* Name.Variable.Global */
60 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */
61 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
62 |
63 | .type-csharp .highlight .k { color: #0000FF }
64 | .type-csharp .highlight .kt { color: #0000FF }
65 | .type-csharp .highlight .nf { color: #000000; font-weight: normal }
66 | .type-csharp .highlight .nc { color: #2B91AF }
67 | .type-csharp .highlight .nn { color: #000000 }
68 | .type-csharp .highlight .s { color: #A31515 }
69 | .type-csharp .highlight .sc { color: #A31515 }
70 |
--------------------------------------------------------------------------------
/src/main/site/stylesheets/screen.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 | /* Welcome to Compass.
4 | * In this file you should write your main styles. (or centralize your imports)
5 | * Import this file using the following HTML or equivalent:
6 | * */
7 |
8 | @import "compass/reset";
9 |
--------------------------------------------------------------------------------
/src/test/scala/io/backchat/hookup/examples/ServerConfigurationsExample.scala:
--------------------------------------------------------------------------------
1 | package io.backchat.hookup
2 | package examples
3 |
4 | import org.specs2.Specification
5 | import org.specs2.time.NoTimeConversions
6 | import scala.concurrent.duration._
7 | import akka.testkit._
8 | import org.json4s.{Formats, DefaultFormats}
9 | import akka.util.Timeout
10 | import java.net.{InetSocketAddress, SocketAddress, ServerSocket, Socket}
11 | import java.io.{BufferedReader, PrintWriter, InputStreamReader}
12 | import java.util.concurrent.{TimeUnit, CountDownLatch, TimeoutException}
13 | import scala.concurrent.{Future, Await}
14 |
15 |
16 | class NoopWireformat(val name: String, val supportsAck: Boolean = false) extends WireFormat {
17 |
18 | def parseInMessage(message: String) = null
19 |
20 | def parseOutMessage(message: String) = null
21 |
22 | def render(message: OutboundMessage) = null
23 | }
24 | class ServerConfigurationsExample extends Specification { def is =
25 | "A Server with a ping configuration" ! serverWithPing ^
26 | "A Server with a content compression configuration" ! serverWithContentCompression ^
27 | "A Server with a max frame configuration" ! serverWithMaxFrame ^
28 | "A Server with a ssl configuration" ! serverWithSslSupport ^
29 | "A Server with a subprotocols configuration" ! serverWithSubprotocols ^
30 | "A Server with a flash policy configuration" ! serverWithFlashPolicy ^ end
31 |
32 | import scala.concurrent.ExecutionContext.Implicits.global
33 |
34 | def serverWithPing = {
35 | /// code_ref: server_with_ping
36 | implicit val jsonFormats: Formats = DefaultFormats
37 | implicit val wireFormat: WireFormat = new JsonProtocolWireFormat
38 |
39 | HookupServer(Ping(Timeout(2 minutes))) {
40 | new HookupServerClient {
41 | def receive = { case _ =>}
42 | }
43 | }
44 | /// end_code_ref
45 | success
46 | }
47 |
48 | def serverWithContentCompression = {
49 | /// code_ref: server_with_compression
50 | HookupServer(ContentCompression(2)) {
51 | new HookupServerClient {
52 | def receive = { case _ =>}
53 | }
54 | }
55 | /// end_code_ref
56 | success
57 | }
58 |
59 | def serverWithMaxFrame = {
60 | /// code_ref: server_with_max_frame
61 | HookupServer(MaxFrameSize(512*1024)) {
62 | new HookupServerClient {
63 | def receive = { case _ =>}
64 | }
65 | }
66 | /// end_code_ref
67 | success
68 | }
69 |
70 | def serverWithSslSupport = {
71 | try {
72 | /// code_ref: server_with_ssl
73 | val sslSupport =
74 | SslSupport(
75 | keystorePath = "./ssl/keystore.jks",
76 | keystorePassword = "changeme",
77 | algorithm = "SunX509")
78 |
79 | HookupServer(sslSupport) {
80 | new HookupServerClient {
81 | def receive = { case _ =>}
82 | }
83 | }
84 | /// end_code_ref
85 | } catch {
86 | case _: Throwable =>
87 | }
88 | success
89 | }
90 |
91 |
92 |
93 | def serverWithSubprotocols = {
94 | /// code_ref: server_with_subprotocols
95 | // these wire formats aren't actually implemented it's just to show the idea
96 | HookupServer(SubProtocols(new NoopWireformat("irc"), new NoopWireformat("xmpp"))) {
97 | new HookupServerClient {
98 | def receive = { case _ =>}
99 | }
100 | }
101 | /// end_code_ref
102 | success
103 | }
104 |
105 | def serverWithFlashPolicy = {
106 | val latch = new CountDownLatch(1)
107 | val port = {
108 | val s = new ServerSocket(0);
109 | try { s.getLocalPort } finally { s.close() }
110 | }
111 | import HookupClient.executionContext
112 | /// code_ref: server_with_flash_policy
113 | val server = HookupServer(port, FlashPolicy("*.example.com", Seq(80, 443, 8080, 8843, port))) {
114 | new HookupServerClient {
115 | def receive = { case _ =>}
116 | }
117 | }
118 | /// end_code_ref
119 | server onStart {
120 | latch.countDown
121 | }
122 | server.start
123 | latch.await(2, TimeUnit.SECONDS) must beTrue and {
124 | val socket = new Socket
125 | socket.connect(new InetSocketAddress("localhost", port), 2000)
126 | val out = new PrintWriter(socket.getOutputStream)
127 |
128 | val in = new BufferedReader(new InputStreamReader(socket.getInputStream))
129 | out.println("%c" format 0)
130 | out.flush()
131 | val recv = Future {
132 | val sb = new Array[Char](159)
133 | var line = in.read(sb)
134 | val resp = new String(sb)
135 | resp
136 | }
137 |
138 | val res = Await.result(recv, 3 seconds)
139 | in.close()
140 | out.close()
141 | socket.close()
142 | server.stop
143 | res must contain("*.example.com") and (res must contain(port.toString))
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/test/scala/io/backchat/hookup/tests/FileBufferSpec.scala:
--------------------------------------------------------------------------------
1 | package io.backchat.hookup
2 | package tests
3 |
4 | import org.specs2.specification.AfterAll
5 | import org.specs2.time.NoTimeConversions
6 | import org.specs2.Specification
7 | import java.io.File
8 | import org.apache.commons.io.{FilenameUtils, FileUtils}
9 | import org.json4s._
10 | import scala.io.Source
11 | import collection.JavaConverters._
12 | import org.specs2.specification.core.{Fragments}
13 | import java.util.concurrent.{Executors, ConcurrentLinkedQueue}
14 | import scala.concurrent.{Await, Future, ExecutionContext}
15 | import scala.concurrent.duration._
16 | import akka.actor.ActorSystem
17 | import collection.mutable.{ArrayBuffer, Buffer, SynchronizedBuffer, ListBuffer}
18 | import java.util.concurrent.atomic.AtomicInteger
19 |
20 | class FileBufferSpec extends Specification with AfterAll { def is =
21 | "A FileBuffer should" ^
22 | "create the path to the file if it doesn't exist" ! createsPath ^
23 | "write to a file while the buffer is open" ! writesToFile ^
24 | "write to memory buffer while draining" ! writesToMemory ^
25 | "drain the buffers" ! drainsBuffers ^
26 | "not fail under concurrent load" ! handlesConcurrentLoads ^
27 | end
28 |
29 | implicit val wireFormat: WireFormat = new JsonProtocolWireFormat()(DefaultFormats)
30 | implicit val executionContext = ExecutionContext.fromExecutorService(Executors.newCachedThreadPool())
31 |
32 |
33 | override def afterAll(): Unit = executionContext.shutdown()
34 |
35 | def createsPath = {
36 | val logPath = new File("./test-work/testing/and/such/buffer.log")
37 | val workPath = new File("./test-work")
38 | if (workPath.exists()) FileUtils.deleteDirectory(workPath)
39 | val buff = new FileBuffer(logPath)
40 | buff.open()
41 | val res = logPath.getParentFile.exists must beTrue
42 | FileUtils.deleteDirectory(workPath)
43 | buff.close()
44 | res
45 | }
46 |
47 | def writesToFile = {
48 | FileUtils.deleteQuietly(new File("./test-work2"))
49 | val logPath = new File("./test-work2/buffer.log")
50 | val buff = new FileBuffer(logPath)
51 | val exp1: OutboundMessage = TextMessage("the first message")
52 | val exp2: OutboundMessage = TextMessage("the second message")
53 | buff.open()
54 | buff.write(exp1)
55 | buff.write(exp2)
56 | buff.close()
57 | val lines = Source.fromFile(logPath).getLines().toList map wireFormat.parseOutMessage
58 | FileUtils.deleteQuietly(new File("./test-work2"))
59 | lines must contain(eachOf(exp1, exp2))
60 | }
61 |
62 | def writesToMemory = {
63 | val logPath = new File("./test-work3/buffer.log")
64 | val exp1: OutboundMessage = TextMessage("the first message")
65 | val exp2: OutboundMessage = TextMessage("the second message")
66 | val queue = new ConcurrentLinkedQueue[String]()
67 | val buff = new FileBuffer(logPath, false, queue)
68 | buff.open()
69 | buff.write(exp1)
70 | buff.write(exp2)
71 | val lst = queue.asScala.toList
72 | buff.close()
73 | FileUtils.deleteDirectory(new File("./test-work3"))
74 | lst must contain(eachOf(wireFormat.render(exp1), wireFormat.render(exp2)))
75 | }
76 |
77 | def drainsBuffers = {
78 | val logPath = new File("./test-work4/buffer.log")
79 | val buff = new FileBuffer(logPath)
80 | val exp1: OutboundMessage = TextMessage("the first message")
81 | val exp2: OutboundMessage = TextMessage("the second message")
82 | buff.open()
83 | buff.write(exp1)
84 | buff.write(exp2)
85 | val lines = new ListBuffer[OutboundMessage]
86 | Await.ready(buff drain { out =>
87 | Future {
88 | lines += out
89 | Success
90 | }
91 | }, 5 seconds)
92 | buff.close()
93 | FileUtils.deleteQuietly(new File("./test-work4"))
94 | lines must contain(eachOf(exp1, exp2))
95 | }
96 |
97 | def handlesConcurrentLoads = {
98 | val system = ActorSystem("filebufferconc")
99 | val logPath = new File("./test-work5/buffer.log")
100 | val buff = new FileBuffer(logPath)
101 | val lines = new ArrayBuffer[OutboundMessage] with SynchronizedBuffer[OutboundMessage]
102 | buff.open()
103 | val reader = system.scheduler.schedule(50 millis, 50 millis) {
104 | Await.ready(buff drain { out =>
105 | Future {
106 | lines += out
107 | Success
108 | }
109 | }, 5 seconds)
110 | }
111 | (1 to 20000) foreach { s =>
112 | buff.write(TextMessage("message %s" format s))
113 | }
114 | reader.cancel()
115 | Await.ready(buff drain { out =>
116 | Future {
117 | lines += out
118 | Success
119 | }
120 | }, 5 seconds)
121 | buff.close()
122 | FileUtils.deleteDirectory(new File("./test-work5"))
123 | system.shutdown()
124 | lines must haveSize(20000)
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/test/scala/io/backchat/hookup/tests/HookupClientSpec.scala:
--------------------------------------------------------------------------------
1 | package io.backchat.hookup
2 | package tests
3 |
4 | import org.specs2.Specification
5 | import org.specs2.specification.{Around, AfterAll}
6 | import org.specs2.time.NoTimeConversions
7 | import org.json4s._
8 | import org.specs2.execute.Result
9 | import org.specs2.execute.AsResult
10 | import java.net.{ServerSocket, URI}
11 | import akka.testkit._
12 | import akka.actor.ActorSystem
13 | import scala.concurrent.duration._
14 | import org.specs2.specification.core.Fragments
15 | import scala.concurrent.{ExecutionContext, Await}
16 | import scala.concurrent.forkjoin.ForkJoinPool
17 | import java.lang.Thread.UncaughtExceptionHandler
18 | import java.util.concurrent.{TimeUnit, TimeoutException}
19 |
20 | object HookupClientSpecification {
21 |
22 | def newExecutionContext() = ExecutionContext.fromExecutorService(new ForkJoinPool(
23 | Runtime.getRuntime.availableProcessors(),
24 | ForkJoinPool.defaultForkJoinWorkerThreadFactory,
25 | new UncaughtExceptionHandler {
26 | def uncaughtException(t: Thread, e: Throwable) {
27 | e.printStackTrace()
28 | }
29 | },
30 | true))
31 |
32 | def newServer(port: Int, defaultProtocol: String = "jsonProtocol"): HookupServer = {
33 | val executor = newExecutionContext()
34 | val serv = HookupServer(
35 | ServerInfo(
36 | name = "Test Echo Server",
37 | defaultProtocol = defaultProtocol,
38 | listenOn = "127.0.0.1",
39 | port = port,
40 | executionContext = executor)) {
41 | new HookupServerClient {
42 | def receive = {
43 | case TextMessage(text) ⇒ send(text)
44 | case JsonMessage(json) => send(json)
45 | }
46 | }
47 | }
48 | serv.onStop {
49 | executor.shutdown()
50 | executor.awaitTermination(5, TimeUnit.SECONDS)
51 | }
52 | serv
53 | }
54 | }
55 |
56 | trait HookupClientSpecification {
57 |
58 |
59 | val serverAddress = {
60 | val s = new ServerSocket(0);
61 | try { s.getLocalPort } finally { s.close() }
62 | }
63 | def server: Server
64 |
65 | type Handler = PartialFunction[(HookupClient, InboundMessage), Any]
66 |
67 | val uri = new URI("ws://127.0.0.1:"+serverAddress.toString+"/")
68 | val clientExecutor = HookupClientSpecification.newExecutionContext()
69 | val defaultClientConfig = HookupClientConfig(
70 | uri,
71 | defaultProtocol = new JsonProtocolWireFormat()(DefaultFormats),
72 | executionContext = clientExecutor)
73 | def withWebSocket[T <% Result](handler: Handler, config: HookupClientConfig = defaultClientConfig)(t: HookupClient => T) = {
74 | val client = new HookupClient {
75 |
76 | val settings = config
77 | def receive = {
78 | case m => handler.lift((this, m))
79 | }
80 | }
81 | Await.ready(client.connect(), 5 seconds)
82 | try { t(client) } finally {
83 | try {
84 | Await.ready(client.disconnect(), 2 seconds)
85 | clientExecutor.shutdownNow()
86 | } catch { case e: Throwable => e.printStackTrace() }
87 | }
88 | }
89 |
90 | }
91 |
92 | class HookupClientSpec extends Specification with AfterAll { def is =
93 | "A WebSocketClient should" ^
94 | "when configured with jsonProtocol" ^
95 | "connect to a server" ! specify("jsonProtocol").connectsToServer ^
96 | "exchange json messages with the server" ! specify("jsonProtocol").exchangesJsonMessages ^ bt ^
97 | "when configured with simpleJsonProtocol" ^
98 | "connect to a server" ! specify("simpleJson").connectsToServerSimpleJson ^
99 | "exchange json messages with the server" ! specify("simpleJson").exchangesJsonMessagesSimpleJson ^ bt ^
100 | "when client requests simpleJson and server is jsonProtocol" ^
101 | "connect to a server" ! specify("jsonProtocol").connectsToServerSimpleJson ^
102 | "exchange json messages with the server" ! specify("jsonProtocol").connectsToServerSimpleJson ^ bt ^
103 | "when client requests jsonProtocol and server is simpleJson" ^
104 | "connect to a server" ! specify("simpleJson").connectsToServer ^
105 | "exchange json messages with the server" ! specify("simpleJson").exchangesJsonMessages ^
106 | end
107 |
108 | implicit val system: ActorSystem = ActorSystem("HookupClientSpec")
109 |
110 | def stopActorSystem() = {
111 | system.shutdown()
112 | system.awaitTermination(5 seconds)
113 | }
114 |
115 | def afterAll() { stopActorSystem() }
116 |
117 | def specify(proto: String) = new ClientSpecContext(proto)
118 |
119 | class ClientSpecContext(defaultProtocol: String) extends HookupClientSpecification with Around {
120 |
121 | val server = HookupClientSpecification.newServer(serverAddress, defaultProtocol)
122 |
123 | def around[T: AsResult](t: =>T) = {
124 | server.start
125 | val r = AsResult(t)
126 | server.stop
127 | r
128 | }
129 |
130 | def connectsToServer = this {
131 | val latch = TestLatch()
132 | withWebSocket({
133 | case (_, Connected) => latch.open()
134 | }) { _ => Await.result(latch, 5 seconds) must not(throwA[TimeoutException]) }
135 | }
136 |
137 | def exchangesJsonMessages = this {
138 | val latch = TestLatch()
139 | withWebSocket({
140 | case (client, Connected) => client send JObject(JField("hello", JString("world")) :: Nil)
141 | case (client, JsonMessage(JObject(JField("hello", JString("world")) :: Nil))) => latch.open
142 | }) { _ => Await.result(latch, 5 seconds) must not(throwA[TimeoutException]) }
143 | }
144 |
145 | def connectsToServerSimpleJson = this {
146 | val latch = TestLatch()
147 | withWebSocket({
148 | case (_, Connected) => latch.open()
149 | }, HookupClientConfig(uri)) { _ => Await.result(latch, 5 seconds) must not(throwA[TimeoutException]) }
150 | }
151 |
152 | def exchangesJsonMessagesSimpleJson = this {
153 | val latch = TestLatch()
154 | withWebSocket({
155 | case (client, Connected) => client send JObject(JField("hello", JString("world")) :: Nil)
156 | case (client, JsonMessage(JObject(JField("hello", JString("world")) :: Nil))) => latch.open
157 | }, HookupClientConfig(uri)) { _ => Await.result(latch, 5 seconds) must not(throwA[TimeoutException]) }
158 | }
159 |
160 | def pendingSpec = pending
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/test/scala/io/backchat/hookup/tests/JsonProtocolWireFormatSpec.scala:
--------------------------------------------------------------------------------
1 | package io.backchat.hookup
2 | package tests
3 |
4 | import org.specs2.Specification
5 | import org.json4s._
6 | import JsonDSL._
7 | import scala.concurrent.duration._
8 | import org.specs2.time.NoTimeConversions
9 |
10 | class JsonProtocolWireFormatSpec extends Specification { def is =
11 | "A JsonProtocolWireFormat should" ^
12 | testWireFormat("text", textMessage, text, text) ^
13 | testWireFormat("json", jsonMessage, json, json) ^
14 | testWireFormat("json array", jsonArrayMessage, jsonArray, jsonArray) ^
15 | testWireFormat("ack", ackMessage, ack, ack) ^
16 | "parse an ack request message" ! { wf.parseInMessage(ackRequestMessage) must_== ackRequest } ^
17 | "build a needs ack message" ! { wf.parseOutMessage(needsAckMessage) must_== needsAck } ^
18 | "render a needs ack message" ! { wf.render(needsAck) must_== needsAckMessage }
19 | end
20 |
21 | val wf = new JsonProtocolWireFormat()(DefaultFormats)
22 |
23 | val textMessage = """{"type":"text","content":"this is a text message"}"""
24 | val text = TextMessage("this is a text message")
25 |
26 | val jsonMessage = """{"type":"json","content":{"data":"a json message"}}"""
27 | val json = JsonMessage(("data" -> "a json message"))
28 |
29 | val jsonArrayMessage = """{"type":"json","content":["data","a json message"]}"""
30 | val jsonArray = JsonMessage(List("data", "a json message"))
31 |
32 | val ackMessage = """{"type":"ack","id":3}"""
33 | val ack = Ack(3L)
34 |
35 | val ackRequestMessage = """{"type":"ack_request","id":3,"content":"this is a text message"}"""
36 | val ackRequest = AckRequest(text, 3)
37 |
38 | val needsAckMessage = """{"type":"needs_ack","timeout":5000,"content":{"type":"text","content":"this is a text message"}}"""
39 | val needsAck: OutboundMessage = NeedsAck(text, 5.seconds)
40 |
41 | def testWireFormat(name: String, serialized: String, in: InboundMessage, out: OutboundMessage) =
42 | "parse a %s message".format(name) ! { wf.parseInMessage(serialized) must_== in } ^
43 | "build a %s message".format(name) ! { wf.parseOutMessage(serialized) must_== out } ^
44 | "render a %s message".format(name) ! { wf.render(out) must_== serialized }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/work/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/backchatio/hookup/2913794eb45d90d65713c9fd631b427abcca2d05/work/.gitkeep
--------------------------------------------------------------------------------