cfrederick-titanium 0.5.1
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.
- data/LICENSE +19 -0
- data/README.rdoc +5 -0
- data/lib/base.rb +49 -0
- data/lib/configuration.rb +59 -0
- data/lib/resource.rb +144 -0
- data/lib/server.rb +262 -0
- data/lib/sqlite_adapter.rb +337 -0
- data/lib/titanium.rb +30 -0
- metadata +96 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009 Cody Frederick
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
data/lib/base.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Titanium
|
22
|
+
|
23
|
+
class Base
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def log_error(progname=nil, &block)
|
28
|
+
Titanium::Configuration.logger.error(progname, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_warn(progname=nil, &block)
|
32
|
+
Titanium::Configuration.logger.warn(progname, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_info(progname=nil, &block)
|
36
|
+
Titanium::Configuration.logger.info(progname, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def log_debug(progname=nil, &block)
|
40
|
+
Titanium::Configuration.logger.debug(progname, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def environment
|
44
|
+
Titanium::Configuration.environment
|
45
|
+
end
|
46
|
+
|
47
|
+
end # Base
|
48
|
+
|
49
|
+
end # Titanium
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Titanium
|
22
|
+
|
23
|
+
module Configuration
|
24
|
+
|
25
|
+
def self.environment
|
26
|
+
@environment ||= ENV['RACK_ENV'] || 'development'
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.environment=(value)
|
30
|
+
@environment = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.logger
|
34
|
+
@logger ||= Logger.new(STDOUT)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.logger=(value)
|
38
|
+
@logger = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.clean_start
|
42
|
+
@clean_start ||= false
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.clean_start=(value)
|
46
|
+
@clean_start = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.db_adapter
|
50
|
+
@db_adapter ||= SQLiteAdapter
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.db_adapter=(value)
|
54
|
+
@db_adapter = value
|
55
|
+
end
|
56
|
+
|
57
|
+
end # Configuration
|
58
|
+
|
59
|
+
end # Titanium
|
data/lib/resource.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Titanium
|
22
|
+
|
23
|
+
class Resource < Base
|
24
|
+
attr_reader :name, :key
|
25
|
+
attr_accessor :metadata
|
26
|
+
|
27
|
+
# Constructs a new Resource. Additional options can be provided as shown below.
|
28
|
+
# r = Resource.new("book")
|
29
|
+
# r = Resource.new("book", {:metadata => {:title => "Animal Farm"}})
|
30
|
+
# r = Resource.new("book", {:key => "128dfrit12wywbc", :metadata => {:title => "Animal Farm"}})
|
31
|
+
def initialize(name, options={})
|
32
|
+
@name = name
|
33
|
+
@key = options[:key]
|
34
|
+
@metadata = options[:metadata]
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_json
|
38
|
+
json_hash = @metadata.dup
|
39
|
+
json_hash[:key] = @key
|
40
|
+
json_hash.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert
|
44
|
+
db_instance = Configuration.db_adapter.new
|
45
|
+
begin
|
46
|
+
db_instance.connect
|
47
|
+
return db_instance.insert(@name, @metadata)
|
48
|
+
ensure
|
49
|
+
db_instance.disconnect
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update
|
54
|
+
db_instance = Configuration.db_adapter.new
|
55
|
+
begin
|
56
|
+
db_instance.connect
|
57
|
+
return db_instance.update(@key, @metadata)
|
58
|
+
ensure
|
59
|
+
db_instance.disconnect
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete
|
64
|
+
db_instance = Configuration.db_adapter.new
|
65
|
+
begin
|
66
|
+
db_instance.connect
|
67
|
+
return db_instance.delete(@key)
|
68
|
+
ensure
|
69
|
+
db_instance.disconnect
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.find_by_name(name, sort_field=nil, sort_order=nil, limit=nil, offset=nil)
|
74
|
+
db_instance = Configuration.db_adapter.new
|
75
|
+
begin
|
76
|
+
db_instance.connect
|
77
|
+
resources = []
|
78
|
+
result = db_instance.find_by_name_and_conditions(name, nil, nil, sort_field, sort_order, limit, offset)
|
79
|
+
result.each do |row|
|
80
|
+
key = row.delete('key')
|
81
|
+
resources << Resource.new(name, {:key => key, :metadata => row})
|
82
|
+
end
|
83
|
+
return resources
|
84
|
+
ensure
|
85
|
+
db_instance.disconnect
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.count_by_name(name)
|
90
|
+
db_instance = Configuration.db_adapter.new
|
91
|
+
begin
|
92
|
+
db_instance.connect
|
93
|
+
count = db_instance.count_by_name(name)
|
94
|
+
return count
|
95
|
+
ensure
|
96
|
+
db_instance.disconnect
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.delete_all_by_name(name)
|
101
|
+
db_instance = Configuration.db_adapter.new
|
102
|
+
begin
|
103
|
+
db_instance.connect
|
104
|
+
return db_instance.delete_all_by_name(name)
|
105
|
+
ensure
|
106
|
+
db_instance.disconnect
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.find_by_name_and_key(name, key)
|
111
|
+
db_instance = Configuration.db_adapter.new
|
112
|
+
begin
|
113
|
+
db_instance.connect
|
114
|
+
resources = []
|
115
|
+
result = db_instance.find_by_name_and_conditions(name, key)
|
116
|
+
result.each do |row|
|
117
|
+
key = row.delete('key')
|
118
|
+
resources << Resource.new(name, {:key => key, :metadata => row})
|
119
|
+
end
|
120
|
+
return resources
|
121
|
+
ensure
|
122
|
+
db_instance.disconnect
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.find_by_name_and_params(name, params, sort_field=nil, sort_order=nil, limit=nil, offset=nil)
|
127
|
+
db_instance = Configuration.db_adapter.new
|
128
|
+
begin
|
129
|
+
db_instance.connect
|
130
|
+
resources = []
|
131
|
+
result = db_instance.find_by_name_and_conditions(name, nil, params, sort_field, sort_order, limit, offset)
|
132
|
+
result.each do |row|
|
133
|
+
key = row.delete('key')
|
134
|
+
resources << Resource.new(name, {:key => key, :metadata => row})
|
135
|
+
end
|
136
|
+
return resources
|
137
|
+
ensure
|
138
|
+
db_instance.disconnect
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end # Resource
|
143
|
+
|
144
|
+
end # Titanium
|
data/lib/server.rb
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rubygems'
|
22
|
+
require 'rack'
|
23
|
+
require 'json/pure'
|
24
|
+
|
25
|
+
module Titanium
|
26
|
+
|
27
|
+
class Server < Base
|
28
|
+
include Rack::Utils
|
29
|
+
|
30
|
+
# Constructs a new Application. This effectively starts the entire web server process which also includes
|
31
|
+
# initializing all other downstream processes such as the database layer.
|
32
|
+
def initialize(root)
|
33
|
+
@app_root = root
|
34
|
+
@file_server = Rack::File.new(@app_root)
|
35
|
+
erase_db if Configuration.clean_start
|
36
|
+
start_db
|
37
|
+
end
|
38
|
+
|
39
|
+
def erase_db
|
40
|
+
db_instance = Configuration.db_adapter.new
|
41
|
+
begin
|
42
|
+
db_instance.erase_db
|
43
|
+
rescue Exception => ex
|
44
|
+
log_error(ex)
|
45
|
+
return nil
|
46
|
+
ensure
|
47
|
+
db_instance.disconnect
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def start_db
|
52
|
+
db_instance = Configuration.db_adapter.new
|
53
|
+
begin
|
54
|
+
db_instance.connect
|
55
|
+
db_instance.startup
|
56
|
+
rescue Exception => ex
|
57
|
+
log_error(ex)
|
58
|
+
return nil
|
59
|
+
ensure
|
60
|
+
db_instance.disconnect
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handles the Rack request.
|
65
|
+
def call(env)
|
66
|
+
begin
|
67
|
+
create_rack_request(env)
|
68
|
+
if is_valid_request?
|
69
|
+
if is_static_request?
|
70
|
+
serve_static_request
|
71
|
+
else
|
72
|
+
serve_dynamic_request
|
73
|
+
end
|
74
|
+
else
|
75
|
+
serve_bad_request
|
76
|
+
end
|
77
|
+
rescue Exception => ex
|
78
|
+
log_error(ex)
|
79
|
+
serve_internal_server_error
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# Constructs a new Rack::Request. This also updates the path_info to the index.html page if no resource is
|
86
|
+
# requested.
|
87
|
+
def create_rack_request(env)
|
88
|
+
@request = Rack::Request.new(env)
|
89
|
+
@request.path_info = '/index.html' if @request.path_info == '/'
|
90
|
+
end
|
91
|
+
|
92
|
+
# Determines if the request is valid.
|
93
|
+
# * Checks the http method verb
|
94
|
+
def is_valid_request?
|
95
|
+
is_valid_method?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Determines if the http request method is acceptable. Only GET, POST, PUT, and DELETE are acceptable.
|
99
|
+
def is_valid_method?
|
100
|
+
case @request.request_method
|
101
|
+
when 'GET', 'POST', 'PUT', 'DELETE' then true
|
102
|
+
else false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Determines if the request is for a static resource. All static requests must be for a resource with a file
|
107
|
+
# extension
|
108
|
+
def is_static_request?
|
109
|
+
get_resource_from_request =~ /\..*$/
|
110
|
+
end
|
111
|
+
|
112
|
+
# Forms a static file path from the request's path information.
|
113
|
+
def get_static_path_from_request
|
114
|
+
"#{@app_root}#{@request.path_info}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# Serves a 400 status code.
|
118
|
+
def serve_bad_request
|
119
|
+
serve_error_code_response(400, "Bad Request")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Serves a 404 status code.
|
123
|
+
def serve_not_found
|
124
|
+
serve_error_code_response(404, "Not Found")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Serves a 500 status code.
|
128
|
+
def serve_internal_server_error
|
129
|
+
serve_error_code_response(500, "Internal Server Error")
|
130
|
+
end
|
131
|
+
|
132
|
+
# Serves an error page (or default message if the page cannot be found) for a given response code
|
133
|
+
def serve_error_code_response(code, message)
|
134
|
+
if File.exist?("#{@app_root}/#{code}.html")
|
135
|
+
@request.path_info = "/#{code}.html"
|
136
|
+
@file_server.call(@request.env)
|
137
|
+
else
|
138
|
+
[code, { 'Content-Type' => 'text/plain' }, message]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Extracts the resource requested. The resource equals the entire path info minus the query string
|
143
|
+
def get_resource_from_request
|
144
|
+
@request.path_info.split("@").first
|
145
|
+
end
|
146
|
+
|
147
|
+
# Extracts the key of the resource requested.
|
148
|
+
# /application/model/book => nil
|
149
|
+
# /library/book@4 => 4
|
150
|
+
def get_key_from_request
|
151
|
+
@request.path_info.include?("@") ? @request.path_info.split("@").last : nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Serves a static request.
|
155
|
+
def serve_static_request
|
156
|
+
if File.exist?(get_static_path_from_request)
|
157
|
+
@file_server.call(@request.env)
|
158
|
+
else
|
159
|
+
serve_not_found
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Serves a dynamic request.
|
164
|
+
def serve_dynamic_request
|
165
|
+
response_hash = self.send(@request.request_method.strip.downcase)
|
166
|
+
@response = Rack::Response.new
|
167
|
+
@response.headers['Content-Type'] = "application/json"
|
168
|
+
@response.write(response_hash.to_json)
|
169
|
+
@response.finish
|
170
|
+
end
|
171
|
+
|
172
|
+
# Updates a resource in the datastore. Called from serve_dynamic_request.
|
173
|
+
def put
|
174
|
+
resource_name = get_resource_from_request
|
175
|
+
resource_key = get_key_from_request
|
176
|
+
metadata = parse_query(@request.body.read)
|
177
|
+
if resource_key
|
178
|
+
resources = Resource.find_by_name_and_key(resource_name, resource_key)
|
179
|
+
if resources && resources.first
|
180
|
+
resource = resources.first
|
181
|
+
resource.metadata = metadata
|
182
|
+
key = resource.update
|
183
|
+
if key
|
184
|
+
{:status => :success, :key => key}
|
185
|
+
else
|
186
|
+
{:status => :failed, :key => nil}
|
187
|
+
end
|
188
|
+
else
|
189
|
+
{:status => :failed, :message => "Unable to find resource with the key: #{resource_key}"}
|
190
|
+
end
|
191
|
+
else
|
192
|
+
serve_bad_request
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Retreives a list of resources. Called from serve_dynamic_request.
|
197
|
+
def get
|
198
|
+
resource_name = get_resource_from_request
|
199
|
+
resource_key = get_key_from_request
|
200
|
+
params = @request.GET
|
201
|
+
sort_field = params.delete('sort-field')
|
202
|
+
sort_order = params.delete('sort-order')
|
203
|
+
limit = params.delete('limit')
|
204
|
+
offset = params.delete('offset')
|
205
|
+
if resource_key
|
206
|
+
if resource_key == "_count"
|
207
|
+
count = Resource.count_by_name(resource_name)
|
208
|
+
else
|
209
|
+
resources = Resource.find_by_name_and_key(resource_name, resource_key)
|
210
|
+
end
|
211
|
+
else
|
212
|
+
if params.nil? or params.empty?
|
213
|
+
resources = Resource.find_by_name(resource_name, sort_field, sort_order, limit, offset)
|
214
|
+
else
|
215
|
+
resources = Resource.find_by_name_and_params(resource_name, params, sort_field, sort_order, limit, offset)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
{:status => :success, :entities => resources, :count => count || resources.length}
|
219
|
+
end
|
220
|
+
|
221
|
+
# Inserts the resource into the datastore. Called from serve_dynamic_request.
|
222
|
+
def post
|
223
|
+
resource_name = get_resource_from_request
|
224
|
+
metadata = @request.POST
|
225
|
+
metadata.delete("key")
|
226
|
+
resource = Resource.new(resource_name, {:metadata => metadata})
|
227
|
+
key = resource.insert
|
228
|
+
if key
|
229
|
+
{:status => :success, :key => key}
|
230
|
+
else
|
231
|
+
{:status => :failed, :key => nil}
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Deletes a resource in the datastore. Called from serve_dynamic_request.
|
236
|
+
def delete
|
237
|
+
resource_name = get_resource_from_request
|
238
|
+
resource_key = get_key_from_request
|
239
|
+
if resource_key
|
240
|
+
if resource_key == "_all"
|
241
|
+
if Resource.delete_all_by_name(resource_name)
|
242
|
+
{:status => :success}
|
243
|
+
else
|
244
|
+
{:status => :failed, :message => "Failed to delete all entities of type: #{resource_name}"}
|
245
|
+
end
|
246
|
+
else
|
247
|
+
resources = Resource.find_by_name_and_key(resource_name, resource_key)
|
248
|
+
if resources && resources.first
|
249
|
+
resource = resources.first
|
250
|
+
resource.delete ? {:status => :success} : {:status => :failed}
|
251
|
+
else
|
252
|
+
{:status => :failed, :message => "Unable to find resource with the key: #{resource_key}"}
|
253
|
+
end
|
254
|
+
end
|
255
|
+
else
|
256
|
+
serve_bad_request
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end # Server
|
261
|
+
|
262
|
+
end # Titanium
|
@@ -0,0 +1,337 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rubygems'
|
22
|
+
require 'time'
|
23
|
+
require 'sqlite3'
|
24
|
+
|
25
|
+
include SQLite3
|
26
|
+
|
27
|
+
module Titanium
|
28
|
+
|
29
|
+
class SQLiteAdapter < Base
|
30
|
+
attr_reader :db_file
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@db_file = "data/#{environment}.db"
|
34
|
+
end
|
35
|
+
|
36
|
+
def erase_db
|
37
|
+
File.delete(File.expand_path(@db_file))
|
38
|
+
end
|
39
|
+
|
40
|
+
def connect
|
41
|
+
@db = Database.new(@db_file)
|
42
|
+
@db.results_as_hash = true
|
43
|
+
end
|
44
|
+
|
45
|
+
def disconnect
|
46
|
+
if @db
|
47
|
+
@db.close unless @db.closed?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def startup
|
52
|
+
@db.transaction do
|
53
|
+
@db.execute(sql_create_types_table)
|
54
|
+
@db.execute(sql_create_resources_table)
|
55
|
+
@db.execute(sql_create_attributes_table)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def insert(name, metadata)
|
60
|
+
resource_key = nil
|
61
|
+
@db.transaction do
|
62
|
+
type_id = @db.get_first_value(sql_find_type_id_from_type_name, :name => name)
|
63
|
+
if type_id.nil?
|
64
|
+
@db.execute(sql_insert_type, :name => name)
|
65
|
+
type_id = @db.last_insert_row_id
|
66
|
+
end
|
67
|
+
@db.execute(sql_insert_resource, :type_id => type_id)
|
68
|
+
resource_key = @db.last_insert_row_id
|
69
|
+
now = Time.now.iso8601
|
70
|
+
metadata['created_at'] = now
|
71
|
+
metadata['updated_at'] = now
|
72
|
+
metadata.each do |key,value|
|
73
|
+
@db.execute(sql_insert_attribute, :resource_key => resource_key, :key => key, :value => value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
return resource_key
|
77
|
+
end
|
78
|
+
|
79
|
+
def update(resource_key, metadata)
|
80
|
+
@db.transaction do
|
81
|
+
@db.execute(sql_delete_attributes_by_resource_key, :resource_key => resource_key)
|
82
|
+
metadata['created_at'] ||= Time.now.iso8601
|
83
|
+
metadata['updated_at'] = Time.now.iso8601
|
84
|
+
metadata.each do |key,value|
|
85
|
+
@db.execute(sql_insert_attribute, :resource_key => resource_key, :key => key, :value => value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
return resource_key
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete(resource_key)
|
92
|
+
@db.transaction do
|
93
|
+
resources_remaining = @db.get_first_value(sql_count_of_resources_by_resource_key, :resource_key => resource_key)
|
94
|
+
@db.execute(sql_delete_attributes_by_resource_key, :resource_key => resource_key)
|
95
|
+
@db.execute(sql_delete_type_by_resource_key, :resource_key => resource_key) if resources_remaining.to_i == 1
|
96
|
+
@db.execute(sql_delete_resource_by_resource_key, :resource_key => resource_key)
|
97
|
+
end
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
|
101
|
+
def count_by_name(name)
|
102
|
+
return @db.get_first_value(sql_count_of_resources_by_type_name, :name => name)
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete_all_by_name(name)
|
106
|
+
@db.transaction do
|
107
|
+
@db.execute_batch(sql_delete_resources_by_type_name, :name => name)
|
108
|
+
end
|
109
|
+
return true
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_by_name_and_conditions(name, key=nil, conditions=nil, sort_field=nil, sort_order=nil, limit=nil, offset=nil)
|
113
|
+
results = {}
|
114
|
+
query_id = generate_query_id
|
115
|
+
|
116
|
+
@db.transaction do
|
117
|
+
index = 0
|
118
|
+
@db.execute(sql_drop_sorter_table(query_id))
|
119
|
+
@db.execute(sql_create_sorter_table(query_id, name, key, conditions, sort_field, sort_order, limit, offset))
|
120
|
+
@db.execute(sql_query_sorter_table(query_id)) do |row|
|
121
|
+
build_results_hash(results, index, row)
|
122
|
+
index = index + 1
|
123
|
+
end
|
124
|
+
@db.execute(sql_drop_sorter_table(query_id))
|
125
|
+
end
|
126
|
+
|
127
|
+
return build_results_array(results)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def generate_query_id
|
133
|
+
values = [rand(0x0010000), rand(0x0010000), rand(0x0010000), rand(0x0010000), rand(0x0010000), rand(0x1000000), rand(0x1000000)]
|
134
|
+
"%04x%04x%04x%04x%04x%06x%06x" % values
|
135
|
+
end
|
136
|
+
|
137
|
+
def sql_drop_sorter_table(query_id)
|
138
|
+
"DROP TABLE IF EXISTS tmp_#{query_id}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def sql_create_sorter_table(query_id, name, key=nil, conditions=nil, sort_field=nil, sort_order=nil, limit=nil, offset=nil)
|
142
|
+
# resolving variables below instead of in the argument signature because there is a difference between
|
143
|
+
# not supplying the argument and providing a nil value. this adapter relies on resolving nil values
|
144
|
+
# for the arguments in the signature.
|
145
|
+
key = key ? "AND r.id = '#{key}'" : ""
|
146
|
+
sort_field ||="created_at"
|
147
|
+
sort_order ||= "ASC"
|
148
|
+
limit ||= "-1"
|
149
|
+
offset ||="0"
|
150
|
+
sql = ""
|
151
|
+
|
152
|
+
if conditions
|
153
|
+
sql = "%subquery%"
|
154
|
+
conditions.each_pair do |attr_key,attr_value|
|
155
|
+
conditions_sql = <<-SQL
|
156
|
+
SELECT DISTINCT r.id
|
157
|
+
FROM resources r
|
158
|
+
INNER JOIN attributes a ON a.resource_id = r.id
|
159
|
+
WHERE a.key = '#{attr_key}' AND a.value = '#{attr_value}'
|
160
|
+
AND r.id IN (
|
161
|
+
%subquery%
|
162
|
+
)
|
163
|
+
SQL
|
164
|
+
sql.gsub!("%subquery%", conditions_sql)
|
165
|
+
end
|
166
|
+
typesql = <<-SQL
|
167
|
+
SELECT r.id
|
168
|
+
FROM resources r
|
169
|
+
INNER JOIN types t ON t.id = r.type_id
|
170
|
+
WHERE t.name = '#{name}'
|
171
|
+
#{key}
|
172
|
+
SQL
|
173
|
+
sql.gsub!("%subquery%", typesql)
|
174
|
+
else
|
175
|
+
sql = <<-SQL
|
176
|
+
SELECT r.id
|
177
|
+
FROM resources r
|
178
|
+
INNER JOIN types t ON t.id = r.type_id
|
179
|
+
WHERE t.name = '#{name}'
|
180
|
+
#{key}
|
181
|
+
SQL
|
182
|
+
end
|
183
|
+
|
184
|
+
sorter_table_sql = <<-SQL
|
185
|
+
CREATE TEMP TABLE tmp_#{query_id} AS
|
186
|
+
SELECT r.id as resource_id, a.value
|
187
|
+
FROM resources r
|
188
|
+
INNER JOIN attributes a ON a.resource_id = r.id
|
189
|
+
WHERE a.key = '#{sort_field}'
|
190
|
+
AND r.id IN (
|
191
|
+
%subquery%
|
192
|
+
)
|
193
|
+
ORDER BY a.value #{sort_order}
|
194
|
+
LIMIT #{limit}
|
195
|
+
OFFSET #{offset}
|
196
|
+
SQL
|
197
|
+
sorter_table_sql.gsub!("%subquery%", sql)
|
198
|
+
|
199
|
+
return sorter_table_sql
|
200
|
+
end
|
201
|
+
|
202
|
+
def sql_query_sorter_table(query_id)
|
203
|
+
<<-SQL
|
204
|
+
SELECT
|
205
|
+
r.id AS key,
|
206
|
+
a.key AS attr_key,
|
207
|
+
a.value AS attr_value
|
208
|
+
FROM resources r
|
209
|
+
INNER JOIN attributes a ON a.resource_id = r.id
|
210
|
+
INNER JOIN tmp_#{query_id} s ON s.resource_id = r.id
|
211
|
+
ORDER BY s.ROWID ASC;
|
212
|
+
SQL
|
213
|
+
end
|
214
|
+
|
215
|
+
def build_results_hash(results, index, row)
|
216
|
+
key = row['key']
|
217
|
+
attr_key = row['attr_key']
|
218
|
+
attr_value = row['attr_value']
|
219
|
+
results[key] ||= {}
|
220
|
+
results[key]['query_index'] ||= index
|
221
|
+
results[key]['key'] ||= key
|
222
|
+
results[key][attr_key] ||= attr_value
|
223
|
+
end
|
224
|
+
|
225
|
+
def build_results_array(results)
|
226
|
+
sorted_results = results.sort { |a,b| a[1]['query_index'] <=> b[1]['query_index'] }
|
227
|
+
results_array = []
|
228
|
+
sorted_results.each do |item|
|
229
|
+
item[1].delete('query_index')
|
230
|
+
results_array << item[1]
|
231
|
+
end
|
232
|
+
return results_array
|
233
|
+
end
|
234
|
+
|
235
|
+
def sql_create_types_table
|
236
|
+
<<-SQL
|
237
|
+
CREATE TABLE IF NOT EXISTS types (
|
238
|
+
id INTEGER PRIMARY KEY NOT NULL,
|
239
|
+
name TEXT NOT NULL
|
240
|
+
);
|
241
|
+
SQL
|
242
|
+
end
|
243
|
+
|
244
|
+
def sql_create_resources_table
|
245
|
+
<<-SQL
|
246
|
+
CREATE TABLE IF NOT EXISTS resources (
|
247
|
+
id INTEGER PRIMARY KEY NOT NULL,
|
248
|
+
type_id INTEGER REFERENCES types(id)
|
249
|
+
);
|
250
|
+
SQL
|
251
|
+
end
|
252
|
+
|
253
|
+
def sql_create_attributes_table
|
254
|
+
<<-SQL
|
255
|
+
CREATE TABLE IF NOT EXISTS attributes (
|
256
|
+
id INTEGER PRIMARY KEY NOT NULL,
|
257
|
+
resource_id INTEGER REFERENCES resources(id),
|
258
|
+
key TEXT NOT NULL,
|
259
|
+
value TEXT
|
260
|
+
);
|
261
|
+
SQL
|
262
|
+
end
|
263
|
+
|
264
|
+
def sql_insert_type
|
265
|
+
"INSERT INTO types (name) VALUES (:name)"
|
266
|
+
end
|
267
|
+
|
268
|
+
def sql_insert_resource
|
269
|
+
"INSERT INTO resources (type_id) VALUES (:type_id)"
|
270
|
+
end
|
271
|
+
|
272
|
+
def sql_insert_attribute
|
273
|
+
"INSERT INTO attributes (resource_id, key, value) VALUES (:resource_key, :key, :value)"
|
274
|
+
end
|
275
|
+
|
276
|
+
def sql_delete_attributes_by_resource_key
|
277
|
+
"DELETE FROM attributes WHERE resource_id = :resource_key"
|
278
|
+
end
|
279
|
+
|
280
|
+
def sql_delete_resource_by_resource_key
|
281
|
+
"DELETE FROM resources WHERE id = :resource_key"
|
282
|
+
end
|
283
|
+
|
284
|
+
def sql_delete_type_by_resource_key
|
285
|
+
<<-SQL
|
286
|
+
DELETE FROM types
|
287
|
+
WHERE id IN (
|
288
|
+
SELECT type_id FROM resources WHERE id = :resource_key
|
289
|
+
)
|
290
|
+
SQL
|
291
|
+
end
|
292
|
+
|
293
|
+
def sql_count_of_resources_by_resource_key
|
294
|
+
<<-SQL
|
295
|
+
SELECT COUNT(*)
|
296
|
+
FROM resources
|
297
|
+
WHERE type_id IN (
|
298
|
+
SELECT type_id FROM resources WHERE id = :resource_key
|
299
|
+
)
|
300
|
+
SQL
|
301
|
+
end
|
302
|
+
|
303
|
+
def sql_count_of_resources_by_type_name
|
304
|
+
<<-SQL
|
305
|
+
SELECT COUNT(*)
|
306
|
+
FROM resources r
|
307
|
+
INNER JOIN types t ON t.id = r.type_id
|
308
|
+
WHERE t.name = :name
|
309
|
+
SQL
|
310
|
+
end
|
311
|
+
|
312
|
+
def sql_delete_resources_by_type_name
|
313
|
+
<<-SQL
|
314
|
+
DELETE FROM attributes
|
315
|
+
WHERE resource_id IN (
|
316
|
+
SELECT r.id
|
317
|
+
FROM resources r
|
318
|
+
INNER JOIN types t ON t.id = r.type_id
|
319
|
+
WHERE t.name = :name
|
320
|
+
);
|
321
|
+
|
322
|
+
DELETE FROM resources
|
323
|
+
WHERE type_id IN (
|
324
|
+
SELECT id FROM types WHERE name = :name
|
325
|
+
);
|
326
|
+
|
327
|
+
DELETE FROM types WHERE name = :name;
|
328
|
+
SQL
|
329
|
+
end
|
330
|
+
|
331
|
+
def sql_find_type_id_from_type_name
|
332
|
+
"SELECT id FROM types WHERE name = :name"
|
333
|
+
end
|
334
|
+
|
335
|
+
end # SQLiteAdapter
|
336
|
+
|
337
|
+
end # Titanium
|
data/lib/titanium.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Copyright (c) 2009 Cody Frederick
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'logger'
|
22
|
+
require 'base'
|
23
|
+
require 'configuration'
|
24
|
+
require 'server'
|
25
|
+
require 'resource'
|
26
|
+
require 'sqlite_adapter'
|
27
|
+
|
28
|
+
module Titanium
|
29
|
+
VERSION = "0.5.1"
|
30
|
+
end # Titanium
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cfrederick-titanium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Frederick
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rack
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "1.0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sqlite3-ruby
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.4
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: json_pure
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.1.7
|
44
|
+
version:
|
45
|
+
description: Lightweight but powerful RESTful application server
|
46
|
+
email: cody@codingsouthpaw.com
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- README.rdoc
|
53
|
+
- LICENSE
|
54
|
+
files:
|
55
|
+
- README.rdoc
|
56
|
+
- lib/base.rb
|
57
|
+
- lib/configuration.rb
|
58
|
+
- lib/resource.rb
|
59
|
+
- lib/server.rb
|
60
|
+
- lib/sqlite_adapter.rb
|
61
|
+
- lib/titanium.rb
|
62
|
+
- LICENSE
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: https://github.com/cfrederick/titanium
|
65
|
+
licenses:
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --line-numbers
|
69
|
+
- --inline-source
|
70
|
+
- --title
|
71
|
+
- titanium
|
72
|
+
- --main
|
73
|
+
- README.rdoc
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
version:
|
88
|
+
requirements: []
|
89
|
+
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 1.3.5
|
92
|
+
signing_key:
|
93
|
+
specification_version: 2
|
94
|
+
summary: Lightweight but powerful RESTful application server
|
95
|
+
test_files: []
|
96
|
+
|