zy 0.0.1.alfa

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a13b474a2570c278d2cd7e81a9ae5837f760ac47
4
+ data.tar.gz: df95600d55da2e70d543dc414107616a7340d697
5
+ SHA512:
6
+ metadata.gz: 13ee3b9b683efe52dcbc9a15f6a7d256876b8b3edc076080551b3f4689f3d2a1aaf2c443ff69327a4cffa1ee8146a74a8096d492a2e64cb157cbb5cf92ec61e4
7
+ data.tar.gz: f15c7ab8febd69ffa1f031f32080306e5893fd3b3e849e003bbecf47ff14946734501392c73509a8ebc3dcd61905a295ed90497eec91761f6f51136bd3bc2b6a
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Ethan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ # Zy
2
+
3
+ Zy is an application level protocol, defining a structure for requests and replies to a service. It draws from concepts of REST over HTTP.
4
+
5
+ It encodes its structure as JSON, and is intended to be transported over ØMQ (ZeroMQ).
6
+
7
+ A request from a client consists of a message with two frames, and an example looks like:
8
+
9
+ ```
10
+ zy 0.0 json
11
+ ```
12
+
13
+ ```json
14
+ {
15
+ "resource": "events",
16
+ "action": "update",
17
+ "params": {
18
+ "name": "2004 summer olympics"
19
+ },
20
+ "body": {
21
+ "location": "Athens, Greece"
22
+ }
23
+ }
24
+ ```
25
+
26
+ The first frame is called the protocol frame and sets up the basics, telling the server that the client is speaking the zy protocol, version 0.0. It indicates that the following message is formatted as JSON (JSON is currently the only supported format).
27
+
28
+ The next frame is the JSON-formatted request. It includes these fields:
29
+
30
+ - resource: the name of the resource. this is analagous to the request path or URI in HTTP.
31
+ - action: the action to be performed. this is analagous to the request method in HTTP.
32
+ - params: parameters of the request. in this case specifying the identity of the resource in question.
33
+ - body: the main message which the server will act on. may not always be specified.
34
+
35
+ A reply to this also consists of two frames and might look like:
36
+
37
+ ```
38
+ zy 0.0 json
39
+ ```
40
+
41
+ ```json
42
+ {
43
+ "status": ["success", "update"],
44
+ "body": {
45
+ "name": "2004 summer olympics",
46
+ "location": "Athens, Greece",
47
+ "year": "2004"
48
+ }
49
+ }
50
+ ```
51
+
52
+ The protocol frame is the same. The reply frame has these fields:
53
+
54
+ - status: an array of strings, each being a short word to identify aspects of the status, in decreasing order of specificity. this is conceptually similar to an HTTP status code, but is human-readable and allows for more precision.
55
+ - body: the main message of the response to be conveyed back to the requester. may not always be specified - in this case, for example, it may have been omitted on the assumption that the client knows the identified resource and does not need a representation of it in the reply after the update.
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new do |t|
3
+ t.name = 'test'
4
+ t.test_files = FileList['test/**/*_test.rb']
5
+ t.verbose = true
6
+ end
7
+ task 'default' => 'test'
@@ -0,0 +1,40 @@
1
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'zy/version'
4
+
5
+ require 'ffi-rzmq'
6
+ require 'json'
7
+
8
+ module Zy
9
+ class Error < StandardError
10
+ end
11
+
12
+ class << self
13
+ def zmq_context
14
+ @zmq_context ||= begin
15
+ ZMQ::Context.new.tap do |zmq_context|
16
+ trap("INT") do
17
+ STDERR.puts "goodbye!"
18
+ zmq_context.terminate
19
+ STDERR.puts "ok exiting"
20
+ exit 0
21
+ STDERR.puts "done"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def zmq_context=(zmq_context)
28
+ if @zmq_context
29
+ raise Zy::Error, "zmq_context is already set"
30
+ else
31
+ @zmq_context = zmq_context
32
+ end
33
+ end
34
+ end
35
+
36
+ autoload :Server, 'zy/server'
37
+ autoload :Request, 'zy/request'
38
+ autoload :Reply, 'zy/reply'
39
+ autoload :Natra, 'zy/natra'
40
+ end
@@ -0,0 +1,50 @@
1
+ module Zy
2
+ class Natra
3
+ def self.inherited(klass)
4
+ klass.instance_variable_set(:@constraints, {})
5
+ klass.instance_variable_set(:@handlers, [])
6
+ end
7
+
8
+ module HandlerContext
9
+ def with_constraints(constraints, &block)
10
+ # keys and values: symbols become strings
11
+ constraints = constraints.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v.is_a?(Symbol) ? v.to_s : v} }.inject({}, &:update)
12
+ begin
13
+ original_constraints = @constraints
14
+ @constraints = @constraints.merge(constraints)
15
+ yield
16
+ ensure
17
+ @constraints = original_constraints
18
+ end
19
+ end
20
+
21
+ def resource(name, &block)
22
+ with_constraints(:resource => name, &block)
23
+ end
24
+
25
+ def action(name = nil, &block)
26
+ if name
27
+ with_constraints(:action => name) { action(&block) }
28
+ else
29
+ @handlers << [@constraints, block]
30
+ end
31
+ end
32
+ end
33
+
34
+ extend HandlerContext
35
+
36
+ def self.call(request)
37
+ _, handler = @handlers.detect do |(constraints, action_block)|
38
+ constraints.all? do |key, value|
39
+ request[key] == value
40
+ end
41
+ end
42
+
43
+ if handler
44
+ handler.call(request)
45
+ else
46
+ {'status' => ['error', 'request', 'unroutable']}
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module Zy
2
+ class Reply
3
+ class << self
4
+ def from(reply_object)
5
+ reply_object.is_a?(self) ? reply_object : new(reply_object)
6
+ end
7
+ end
8
+
9
+ def initialize(object)
10
+ @object = object
11
+ end
12
+
13
+ def reply_strings
14
+ @reply_strings ||= [protocol_string, reply_string]
15
+ end
16
+
17
+ def protocol_string
18
+ 'zy 0.0 json'
19
+ end
20
+
21
+ def reply_string
22
+ begin
23
+ JSON.generate(@object)
24
+ rescue JSON::GeneratorError
25
+ # TODO log debug
26
+ JSON.generate({"status" => ["error", "server", "internal_error"]})
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ require 'json'
2
+
3
+ module Zy
4
+ class Request
5
+ def initialize(request_strings)
6
+ @request_strings = request_strings
7
+ @error_status = catch(:error) do
8
+ # this should not happen; it's not possible to get a 0-frame message in zmq
9
+ throw(:error, ['error', 'request', 'no_frames']) if @request_strings.size < 1
10
+
11
+ @protocol_string = @request_strings[0]
12
+ @protocol_parts = @protocol_string.strip.split(/ +/)
13
+
14
+ # protocol part 0: protocol name (zy)
15
+ part = 0
16
+ if @protocol_parts.size <= part
17
+ throw(:error, ['error', 'protocol', 'protocol_name_not_specified'])
18
+ elsif @protocol_parts[part] != 'zy'
19
+ throw(:error, ['error', 'protocol', 'protocol_not_supported'])
20
+ end
21
+
22
+ # protocol part 1: version
23
+ part = 1
24
+ if @protocol_parts.size <= part
25
+ throw(:error, ['error', 'protocol', 'version_not_specified'])
26
+ elsif @protocol_parts[part] != '0.0'
27
+ throw(:error, ['error', 'protocol', 'version_not_supported'])
28
+ end
29
+
30
+ # protocol part 2: format
31
+ part = 2
32
+ if @protocol_parts.size <= part
33
+ throw(:error, ['error', 'protocol', 'format_not_specified'])
34
+ elsif @protocol_parts[part] != 'json'
35
+ throw(:error, ['error', 'protocol', 'format_not_supported'])
36
+ end
37
+
38
+ # protocol part(s) we don't recognize
39
+ part = 3
40
+ if @protocol_parts.size > part
41
+ throw(:error, ['error', 'protocol', 'too_many_parts'])
42
+ end
43
+
44
+ if @request_strings.size < 2
45
+ throw(:error, ['error', 'request', 'request_not_specified'])
46
+ end
47
+
48
+ @request_string = @request_strings[1]
49
+
50
+ if @request_strings.size > 2
51
+ throw(:error, ['error', 'request', 'too_many_frames'])
52
+ end
53
+
54
+ begin
55
+ @object = JSON.parse(@request_string)
56
+ rescue JSON::ParserError
57
+ throw(:error, ['error', 'request', 'not_in_specified_format'])
58
+ end
59
+
60
+ throw(:error, ['error', 'request', 'not_object']) unless @object.is_a?(Hash)
61
+ end
62
+ end
63
+
64
+ attr_reader :error_status
65
+ attr_reader :object
66
+
67
+ def [](key)
68
+ @object[key]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,101 @@
1
+ module Zy
2
+ class ServerError < Error
3
+ end
4
+
5
+ class Server
6
+ # options may include:
7
+ #
8
+ # - app
9
+ # - bind
10
+ # - connect
11
+ # - server_private_key
12
+ def initialize(options)
13
+ # stringify symbol keys
14
+ @options = options.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
15
+ end
16
+
17
+ def app
18
+ @options['app']
19
+ end
20
+
21
+ def start
22
+ debug({:server_socket => 'creating'})
23
+ server_socket = Zy.zmq_context.socket(ZMQ::REP)
24
+ debug({:server_socket => 'created (ZMQ::REP)'})
25
+ raise(ServerError, "failed to create socket") unless server_socket
26
+
27
+ if @options['server_private_key']
28
+ debug({:server_socket => {:curve => 'setting to server'}})
29
+ rc = server_socket.setsockopt(ZMQ::CURVE_SERVER, 1)
30
+ debug({:server_socket => {:curve => 'set to server'}})
31
+ raise(ServerError, "failed to set server socket to curve server (errno = #{ZMQ::Util.errno})") if rc < 0
32
+ debug({:server_socket => {:curve => 'setting private key'}})
33
+ rc = server_socket.setsockopt(ZMQ::CURVE_SECRETKEY, @options['server_private_key'])
34
+ debug({:server_socket => {:curve => 'set private key'}})
35
+ raise(ServerError, "failed to set server socket curve secret key (errno = #{ZMQ::Util.errno})") if rc < 0
36
+ else
37
+ debug({:server_socket => {:curve => 'private key not specified'}})
38
+ end
39
+
40
+ if @options['bind']
41
+ debug({:server_socket => "binding #{@options['bind']}"})
42
+ bind_rc = server_socket.bind(@options['bind'])
43
+ debug({:server_socket => "bound #{@options['bind']}"})
44
+ raise(ServerError, "failed to bind server socket to #{@options['bind']} (errno = #{ZMQ::Util.errno})") if bind_rc < 0
45
+ elsif @options['connect']
46
+ debug({:server_socket => "connecting #{@options['connect']}"})
47
+ connect_rc = server_socket.connect(@options['connect'])
48
+ debug({:server_socket => "connected #{@options['connect']}"})
49
+ raise(ServerError, "failed to connect server socket to #{@options['connect']} (errno = #{ZMQ::Util.errno})") if connect_rc < 0
50
+ else
51
+ raise(ServerError, "must specify bind or connect address")
52
+ end
53
+
54
+ loop do
55
+ debug({:server_socket => "ready to recv"})
56
+ request_strings = []
57
+ more = true
58
+ while more
59
+ request_message = ZMQ::Message.create || raise(ServerError, "failed to create message (errno = #{ZMQ::Util.errno})")
60
+ recv_rc = server_socket.recvmsg(request_message)
61
+ debug({:server_socket => "recvd (#{recv_rc})"})
62
+ raise(ServerError, "server socket failed to recv (errno = #{ZMQ::Util.errno})") if recv_rc < 0
63
+ request_strings << request_message.copy_out_string
64
+ debug({:server_socket => "copied #{request_strings.last}"})
65
+ request_message.close
66
+ more = server_socket.more_parts?
67
+ end
68
+ request = Request.new(request_strings)
69
+ if request.error_status
70
+ reply_obj = {'status' => request.error_status}
71
+ else
72
+ begin
73
+ reply_obj = app.call(request)
74
+ rescue Exception => e
75
+ debug({exception: {class: e.class, message: e.message, backtrace: e.backtrace}})
76
+ reply_obj = {"status" => ["error", "server", "internal_error"]}
77
+ end
78
+ end
79
+ reply = Reply.from(reply_obj)
80
+ reply.reply_strings.each_with_index do |reply_s, i|
81
+ flags = i < reply.reply_strings.size - 1 ? ZMQ::SNDMORE : 0
82
+ debug({:server_socket => "sending #{reply_s} (flags=#{flags})"})
83
+ send_rc = server_socket.send_string(reply_s, flags)
84
+ debug({:server_socket => "sent (#{send_rc})"})
85
+ raise(ServerError, "server socket failed to send (errno = #{ZMQ::Util.errno})") if send_rc < 0
86
+ end
87
+ end
88
+ end
89
+
90
+ def logger
91
+ @logger ||= @options['logger'] || begin
92
+ require 'logger'
93
+ ::Logger.new(STDOUT)
94
+ end
95
+ end
96
+
97
+ def debug(message)
98
+ logger.debug JSON.generate message
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module Zy
2
+ VERSION = '0.0.1.alfa'
3
+ end
@@ -0,0 +1,130 @@
1
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
2
+ require 'helper'
3
+
4
+ describe(Zy::Server) do
5
+ before do
6
+ server = TCPServer.new('127.0.0.1', 0)
7
+ @server_port = server_port = server.addr[1]
8
+ server.close
9
+
10
+ STDERR.puts "firing server"
11
+ Thread.abort_on_exception = true
12
+ @server_thread = Thread.new do
13
+ Zy::Server.new(
14
+ :app => app,
15
+ :bind => "tcp://*:#{server_port}",
16
+ ).start
17
+ end
18
+ STDERR.puts "fired server"
19
+ end
20
+ after do
21
+ @server_thread.kill
22
+ end
23
+
24
+ let(:app) do
25
+ proc { |request| {} }
26
+ end
27
+
28
+ let(:client_socket) do
29
+ Zy.zmq_context.socket(ZMQ::REQ).tap do |client_socket|
30
+ client_socket.connect("tcp://localhost:#{@server_port}")
31
+ end
32
+ end
33
+
34
+ def request_s(*request_strings)
35
+ request_strings.each_with_index do |request_s, i|
36
+ flags = i < request_strings.size - 1 ? ZMQ::SNDMORE : 0
37
+ send_rc = client_socket.send_string(request_s, flags)
38
+ assert(send_rc >= 0)
39
+ end
40
+ reply_strings = []
41
+ more = true
42
+ while more
43
+ reply_message = ZMQ::Message.create || raise(ServerError, "failed to create message (errno = #{ZMQ::Util.errno})")
44
+ recv_rc = client_socket.recvmsg(reply_message)
45
+ assert(recv_rc >= 0)
46
+ reply_strings << reply_message.copy_out_string
47
+ reply_message.close
48
+ more = client_socket.more_parts?
49
+ end
50
+ assert_equal(2, reply_strings.size)
51
+ assert_equal('zy 0.0 json', reply_strings[0])
52
+ JSON.parse(reply_strings[1])
53
+ end
54
+
55
+ it 'requests and replies' do
56
+ reply_s = request_s('zy 0.0 json', '{}')
57
+ end
58
+
59
+ describe 'internal server error' do
60
+ let(:app) do
61
+ proc { |request| raise }
62
+ end
63
+
64
+ it 'internal server errors' do
65
+ reply = request_s('zy 0.0 json', '{}')
66
+ assert_equal({'status' => ['error', 'server', 'internal_error']}, reply)
67
+ end
68
+ end
69
+
70
+ describe 'reply object not jsonable' do
71
+ let(:app) do
72
+ proc { |request| Object.new }
73
+ end
74
+
75
+ it 'internal server errors' do
76
+ reply = request_s('zy 0.0 json', '{}')
77
+ assert_equal({'status' => ['error', 'server', 'internal_error']}, reply)
78
+ end
79
+ end
80
+
81
+ it 'rejects non-json' do
82
+ reply = request_s('zy 0.0 json', 'hello!')
83
+ assert_equal({'status' => ['error', 'request', 'not_in_specified_format']}, reply)
84
+ end
85
+
86
+ it 'rejects non-object in json' do
87
+ reply = request_s('zy 0.0 json', '["a request"]')
88
+ assert_equal({'status' => ['error', 'request', 'not_object']}, reply)
89
+ end
90
+
91
+ it 'rejects missing request frame' do
92
+ reply = request_s('zy 0.0 json')
93
+ assert_equal({'status' => ['error', 'request', 'request_not_specified']}, reply)
94
+ end
95
+
96
+ it 'rejects too many frames' do
97
+ reply = request_s('zy 0.0 json', '{}', '{}')
98
+ assert_equal({'status' => ['error', 'request', 'too_many_frames']}, reply)
99
+ end
100
+
101
+ it 'rejects missing format' do
102
+ reply = request_s('zy 0.0', '{}')
103
+ assert_equal({'status' => ['error', 'protocol', 'format_not_specified']}, reply)
104
+ end
105
+
106
+ it 'rejects missing version' do
107
+ reply = request_s('zy', '{}')
108
+ assert_equal({'status' => ['error', 'protocol', 'version_not_specified']}, reply)
109
+ end
110
+
111
+ it 'rejects missing protocol' do
112
+ reply = request_s('', '{}')
113
+ assert_equal({'status' => ['error', 'protocol', 'protocol_name_not_specified']}, reply)
114
+ end
115
+
116
+ it 'rejects unrecognized format' do
117
+ reply = request_s('zy 0.0 xml', '{}')
118
+ assert_equal({'status' => ['error', 'protocol', 'format_not_supported']}, reply)
119
+ end
120
+
121
+ it 'rejects unsupported version' do
122
+ reply = request_s('zy 9.0 json', '{}')
123
+ assert_equal({'status' => ['error', 'protocol', 'version_not_supported']}, reply)
124
+ end
125
+
126
+ it 'rejects wrong protocol' do
127
+ reply = request_s('http 0.0 json', '{}')
128
+ assert_equal({'status' => ['error', 'protocol', 'protocol_not_supported']}, reply)
129
+ end
130
+ end
@@ -0,0 +1,10 @@
1
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('../lib', File.dirname(__FILE__)))
2
+
3
+ # NO EXPECTATIONS
4
+ ENV["MT_NO_EXPECTATIONS"] = ''
5
+
6
+ require 'minitest/autorun'
7
+ require 'minitest/reporters'
8
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
9
+
10
+ require 'zy'
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.any? { |lp| File.expand_path(lp) == File.expand_path(lib) }
4
+ require 'zy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'zy'
8
+ spec.version = Zy::VERSION
9
+ spec.authors = ['Ethan']
10
+ spec.email = ['ethan@unth']
11
+ spec.summary = 'zy'
12
+ spec.description = 'zy'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0") - ['.gitignore']
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'ffi-rzmq', '~> 2.0'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'minitest'
24
+ spec.add_development_dependency 'minitest-reporters'
25
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alfa
5
+ platform: ruby
6
+ authors:
7
+ - Ethan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi-rzmq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: zy
70
+ email:
71
+ - ethan@unth
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - Gemfile
77
+ - LICENSE.txt
78
+ - README.md
79
+ - Rakefile.rb
80
+ - lib/zy.rb
81
+ - lib/zy/natra.rb
82
+ - lib/zy/reply.rb
83
+ - lib/zy/request.rb
84
+ - lib/zy/server.rb
85
+ - lib/zy/version.rb
86
+ - test/basic_interaction_test.rb
87
+ - test/helper.rb
88
+ - zy.gemspec
89
+ homepage: ''
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - '>'
105
+ - !ruby/object:Gem::Version
106
+ version: 1.3.1
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.2.2
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: zy
113
+ test_files:
114
+ - test/basic_interaction_test.rb
115
+ - test/helper.rb
116
+ has_rdoc: