nasreddin 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 14b6167ede8abe24650d6a757cf8559cc8e2855f
4
+ data.tar.gz: cb9108d4eb76c807370a626c736ece44260892ac
5
+ SHA512:
6
+ metadata.gz: 83bc41838bb86061abecba46b6d64f101d9135ce620f1d6fe05b22abaf713a571bc4707b2896f82436c1d128c11044394ffbb18250d0699ee9e5f573ef3dcb8a
7
+ data.tar.gz: 18314db0937c622bb89f01cd41e2d86b19a5830e386450b52bc2b867e81e50d95dd5dca6d6d875a3ea7f371cf156d0e56630ffabe0c4db79ad4cff7e575d05f5
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ > "Nasreddin, your donkey has been lost."
2
+
3
+ > "Thank goodness I was not on the donkey at the time, or I would be lost too."
4
+
5
+
6
+ ## What is Nasreddin?
7
+
8
+ Nasreddin Hoca was a wise scolar from 13th century Anatolia. This, however, is a library designed to enable know-nothing
9
+ distribution of data requests between varying services.
10
+
11
+ To connect two services, one needs to include the Nasreddin::APIServer middleware and indicate what resources it is offering:
12
+ ```ruby
13
+ require 'nasreddin/api-server'
14
+
15
+ use Nasreddin::APIServer, resources: %w| users |
16
+ run MyApp
17
+ ```
18
+ Then, the consumer can use the Nasreddin::Resource class to generate new classes that will talk to the APIServer when making
19
+ requests.
20
+
21
+
22
+ An example implementation of Nasreddin::Resource is provided below
23
+
24
+ ```ruby
25
+ require 'nasreddin/resource'
26
+
27
+ class User < Nasreddin::Resource('users') # name of endpoint for resource
28
+ end
29
+
30
+ ```
31
+
32
+
33
+ ## Caveats
34
+
35
+ Currently, Nasreddin relies on JRuby and TorqueBox/HornetQ.
36
+
37
+
38
+ ## License
39
+
40
+ Copyright (C) 2012-2013 Burnside Digital
41
+
42
+ Licensed under the BSD 2-Clause License. See COPYING for license details.
data/lib/nasreddin.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'nasreddin/version'
2
+ require 'nasreddin/resource'
3
+ require 'nasreddin/remote_torquebox_adapter'
4
+ require 'nasreddin/api-server'
@@ -0,0 +1,136 @@
1
+ require 'stringio'
2
+ require 'uri'
3
+
4
+ module Nasreddin
5
+
6
+ class APIServer
7
+
8
+ def initialize(app, options = {})
9
+ @app = app
10
+ @threads = {}
11
+ @resources = options[:resources]
12
+ @route_prefix = options[:route_prefix]
13
+ if @resources
14
+ @resources.each do |resource|
15
+ @threads[resource] = Thread.new { Nasreddin::APIServerResource.new(@route_prefix,resource,@app).run }
16
+ end
17
+ else
18
+ $stderr.puts "WARNING: Nasreddin::APIServer is being used without any resources specified!"
19
+ end
20
+ end
21
+
22
+ def call(env)
23
+ params = Rack::Request.new(env).params
24
+
25
+ if is_heartbeat?(params)
26
+ res = params['resource'] = params['resources'].pop
27
+ params["resources.#{res}"] = @resources
28
+ Nasreddin::Resource(@resource).remote_call(params)
29
+ else
30
+ @app.call(env)
31
+ end
32
+ end
33
+
34
+ def is_heartbeat?(params)
35
+ params.has_key? '__hearbeat__'
36
+ end
37
+ end
38
+
39
+ class APIServerResource
40
+
41
+ DEFAULT_ENV = {
42
+ 'rack.errors' => $stderr,
43
+ 'rack.input' => StringIO.new,
44
+ 'rack.version' => [1, 1],
45
+ 'rack.multithread' => true,
46
+ 'rack.multiprocess' => true,
47
+ 'rack.run_once' => false,
48
+ 'HTTP_ACCEPT' => 'application/json',
49
+ 'HTTP_USER_AGENT' => 'NasreddinAPI'
50
+ }
51
+
52
+ def initialize(route_prefix,resource,app)
53
+ @resource = resource
54
+ @route_prefix = route_prefix
55
+ @app = app
56
+ end
57
+
58
+ def queue
59
+ @queue ||= TorqueBox::Messaging::Queue.start("/queues/#{@resource}", durable: false)
60
+ end
61
+
62
+ def run
63
+ loop do
64
+ begin
65
+ queue.receive_and_publish &method(:process_incoming_message)
66
+ rescue Exception => err
67
+ $stderr.puts "Error processing request: #{err.class} - #{err.message}"
68
+ if err.kind_of?(Java::JavaxJms::JMSException)
69
+ break
70
+ end
71
+ end
72
+ end
73
+ queue.stop
74
+ end
75
+
76
+ def is_heartbeat?(msg)
77
+ msg[:params] && msg[:params].has_key?('__heartbeat__')
78
+ end
79
+
80
+ def call(env)
81
+ @app.call(env)
82
+ end
83
+
84
+ def process_incoming_message(msg)
85
+ return heartbeat_ok if is_heartbeat?(msg)
86
+
87
+ begin
88
+ status, headers, body = call(env(msg))
89
+
90
+ resp = ''
91
+ body.each { |d| resp += d.to_s }
92
+ body.close if body.respond_to?(:close)
93
+
94
+ rescue Exception => err
95
+ resp = "#{err.message}\n\n#{err.backtrace.join("\n")}"
96
+ end
97
+
98
+ [status, headers, resp]
99
+ end
100
+
101
+ def heartbeat_ok
102
+ [200, nil, "OK"]
103
+ end
104
+
105
+ def env(msg)
106
+ env = DEFAULT_ENV.clone
107
+ env['rack.url_scheme'] = (msg.delete(:secure) ? 'https' : 'http')
108
+ method = msg.delete(:method) || 'GET'
109
+ env['REQUEST_METHOD'] = method.to_s.upcase
110
+ env['QUERY_STRING'] = queryize(msg.delete(:params)) || ''
111
+ env['SCRIPT_NAME'] = "/#{@route_prefix}" || ''
112
+ env['PATH_INFO'] = "/#{@resource}"
113
+ if ((id = msg.delete(:id)))
114
+ env['PATH_INFO'] += "/#{id}"
115
+ end
116
+ if ((path = msg.delete(:path)))
117
+ env['PATH_INFO'] += "/#{path}"
118
+ end
119
+ env['REQUEST_URI'] = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
120
+ env.merge!(Hash[msg.map { |k, v| [k.to_s, v] }])
121
+ env
122
+ end
123
+
124
+ private
125
+
126
+ def queryize(params = {})
127
+ params.map do |key, value|
128
+ if value.is_a? Array
129
+ value.map { |v| URI.encode("#{key}[]=#{v}") }
130
+ else
131
+ URI.encode("#{key}=#{value}")
132
+ end
133
+ end.join('&') unless params.nil?
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,38 @@
1
+ #require 'torquebox/messaging'
2
+ require 'multi_json'
3
+
4
+ module Nasreddin
5
+ class RemoteTorqueboxAdapter
6
+ attr_accessor :resource, :klass
7
+
8
+ def load_data(data,resource, as_objects = true)
9
+ resp = MultiJson.load(data)
10
+ resp = resp[@resource] if resp.respond_to?(:keys) && resp.keys.include?(@resource)
11
+ if resp.kind_of? Array
12
+ as_objects ? resp.map { |r| @klass.new(r) } : resp
13
+ else
14
+ as_objects ? @klass.new(resp) : resp
15
+ end
16
+ end
17
+
18
+ def succeded?(status)
19
+ status != nil && status > 199 && status < 300
20
+ end
21
+
22
+ def queue
23
+ @queue ||= TorqueBox::Messaging::Queue.start("/queues/#{@resource}", durable: false)
24
+ end
25
+
26
+ def call(params, as_new_objects=false)
27
+ status, _, data = *(queue.publish_and_receive(params, persistant: false))
28
+ values = load_data(data,@resource,as_new_objects) if data && !data.empty?
29
+ [ succeded?(status), values ]
30
+ end
31
+
32
+ def initialize(resource, klass)
33
+ @resource = resource
34
+ @klass = klass
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,175 @@
1
+ require 'torquebox/messaging'
2
+ require 'multi_json'
3
+
4
+ module Nasreddin
5
+
6
+ class SaveError < Exception ; end
7
+
8
+ # == Nasreddin Resource
9
+ # Provides a base class for implementing an API backed data object.
10
+ # A minimal implementation could be:
11
+ # class Car < Nasreddin::Resource('cars')
12
+ # end
13
+ class Resource
14
+ class<<self
15
+ attr_accessor :resource
16
+
17
+ def subclasses; (@subclasses ||= []); end
18
+
19
+ def inherited(sub)
20
+ subclasses << sub
21
+ sub.resource = @resource
22
+ end
23
+
24
+ def remote
25
+ @remote ||= Nasreddin::RemoteTorqueboxAdapter.new(@resource, self)
26
+ end
27
+
28
+ def remote_call(params = {}, as_objects=true)
29
+ success, values = remote.call(params, as_objects)
30
+ values
31
+ end
32
+
33
+ # Allows fetching of all entities without requiring filtering
34
+ # parameters.
35
+ def all
36
+ remote_call({ method: 'GET' }, true)
37
+ end
38
+
39
+ # Allows searching for a specific entity or a collection of
40
+ # entities that match a certain criteria.
41
+ # example usage:
42
+ # Car.find(15)
43
+ # # => #<Car:0x5fafa486>
44
+ #
45
+ # Car.find(make: 'Ford')
46
+ # # => [ #<Car:0x5fafa486> ]
47
+ #
48
+ # Car.find(15, make: 'Ford')
49
+ # # => [ #<Car:0x5fafa486> ]
50
+ def find(*args)
51
+ params = args.last.kind_of?(Hash) ? args.pop : {}
52
+ id = args.shift
53
+
54
+ remote_call({ method: 'GET', id: id, params: params })
55
+ end
56
+
57
+ # Allows creating a new record in one shot
58
+ # returns true if the record was created
59
+ # example usage:
60
+ # Car.create make: 'Ford', model: 'Focus'
61
+ # # => true or false
62
+ def create(properties = {})
63
+ new(properties).save
64
+ end
65
+
66
+ # Allows destroying a resource without finding it
67
+ # example usage:
68
+ # Car.destroy(15)
69
+ # # => true or false
70
+ def destroy(id)
71
+ remote_call({ method: 'DELETE', id: id }, false).empty?
72
+ end
73
+ end
74
+
75
+ # Custom to_json implementation
76
+ # passes through options to @data
77
+ def to_json(options={})
78
+ @data.to_json(options)
79
+ end
80
+
81
+ # Custom as_json implementation
82
+ # passes through options to @data
83
+ def as_json(options={})
84
+ @data.as_json(options)
85
+ end
86
+
87
+ # Checks if the current instance has
88
+ # already been deleted
89
+ def deleted?
90
+ @deleted
91
+ end
92
+
93
+ # Saves the current resource instance
94
+ # if the instance has an ID it sends a PUT request
95
+ # otherwise it sends a POST request
96
+ # will raise an error if the object has been deleted
97
+ # example usage:
98
+ # car = Car.find(15)
99
+ # car.miles += 1500
100
+ # car.save
101
+ # # => true or false
102
+ def save
103
+ raise SaveError.new("Cannot save a deleted resource") if deleted?
104
+
105
+ if @data['id'].to_s.empty?
106
+ remote_call({ method: 'POST', params: @data })
107
+ else
108
+ remote_call({ method: 'PUT', id: @data['id'], params: @data })
109
+ end
110
+ end
111
+
112
+ def remote_call(params)
113
+ success, values = remote.call(params, false)
114
+ @data = values if values && !values.empty?
115
+ success
116
+ end
117
+
118
+ # Destroys the current resource instance
119
+ # example usage:
120
+ # car = Car.find(15)
121
+ # car.destroy
122
+ # # => true or false
123
+ def destroy
124
+ if !deleted?
125
+ @deleted = remote_call({ method: 'DELETE', id: @data['id'] })
126
+ end
127
+ end
128
+
129
+ # Initialize a new instance
130
+ # also defines setters for any values given
131
+ # example usage:
132
+ # car = Car.new make: 'Ford', model: 'Mustang'
133
+ # car.respond_to? :make=
134
+ # # => true
135
+ def initialize(data={})
136
+ @deleted = false
137
+ @data = data
138
+ @data.each do |key, value|
139
+ unless respond_to?("#{key.to_s}=")
140
+ self.class.send(:define_method, "#{key.to_s}=") do |other|
141
+ @data[key.to_s] = other
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ def method_missing(mid, *args, &block)
148
+ if @data.keys.include?(mid.to_s)
149
+ @data[mid.to_s]
150
+ else
151
+ super
152
+ end
153
+ end
154
+
155
+ def respond_to?(mid, include_private=false)
156
+ @data.keys.include?(mid.to_s) || super
157
+ end
158
+
159
+ private
160
+
161
+ def remote
162
+ self.class.send(:remote)
163
+ end
164
+
165
+ end
166
+
167
+ def self.Resource(name)
168
+ klass = Resource.subclasses.find { |k| k.resource == name }
169
+ unless klass
170
+ klass = Class.new(Resource)
171
+ klass.resource = name
172
+ end
173
+ klass
174
+ end
175
+ end
@@ -0,0 +1,3 @@
1
+ module Nasreddin
2
+ VERSION = '0.3.10'
3
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nasreddin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.10
5
+ platform: ruby
6
+ authors:
7
+ - Josh Ballanco
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: torquebox
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '2.0'
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.4.1
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 1.4.1
39
+ prerelease: false
40
+ type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ name: multi_json
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 1.5.0
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ version: 1.5.0
53
+ prerelease: false
54
+ type: :runtime
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-test
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.6.1
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 0.6.1
67
+ prerelease: false
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ name: torquebox-server
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: '2.0'
81
+ prerelease: false
82
+ type: :development
83
+ - !ruby/object:Gem::Dependency
84
+ name: kramdown
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 0.13.7
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: 0.13.7
95
+ prerelease: false
96
+ type: :development
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 0.8.2
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: 0.8.2
109
+ prerelease: false
110
+ type: :development
111
+ - !ruby/object:Gem::Dependency
112
+ name: bacon
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 1.1.0
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ~>
121
+ - !ruby/object:Gem::Version
122
+ version: 1.1.0
123
+ prerelease: false
124
+ type: :development
125
+ - !ruby/object:Gem::Dependency
126
+ name: mocha-on-bacon
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ prerelease: false
138
+ type: :development
139
+ description: ' Nasreddin is a library to make distributed calls via HornetQ '
140
+ email:
141
+ - jballanc@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ./lib/nasreddin.rb
147
+ - ./lib/nasreddin/api-server.rb
148
+ - ./lib/nasreddin/remote_torquebox_adapter.rb
149
+ - ./lib/nasreddin/resource.rb
150
+ - ./lib/nasreddin/version.rb
151
+ - README.md
152
+ homepage: ''
153
+ licenses: []
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - '>='
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project: ''
171
+ rubygems_version: 2.1.10
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: A library for making distributed calls via HornetQ
175
+ test_files: []
176
+ has_rdoc: