rod-rest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +69 -0
- data/Rakefile +21 -0
- data/Readme.md +3 -0
- data/lib/rod/rest.rb +14 -0
- data/lib/rod/rest/api.rb +100 -0
- data/lib/rod/rest/client.rb +215 -0
- data/lib/rod/rest/collection_proxy.rb +52 -0
- data/lib/rod/rest/constants.rb +5 -0
- data/lib/rod/rest/exception.rb +8 -0
- data/lib/rod/rest/json_serializer.rb +59 -0
- data/lib/rod/rest/metadata.rb +38 -0
- data/lib/rod/rest/naming.rb +20 -0
- data/lib/rod/rest/property_metadata.rb +20 -0
- data/lib/rod/rest/proxy.rb +121 -0
- data/lib/rod/rest/proxy_factory.rb +30 -0
- data/lib/rod/rest/resource_metadata.rb +32 -0
- data/rod-rest.gemspec +32 -0
- data/test/int/end_to_end.rb +138 -0
- data/test/spec/api.rb +210 -0
- data/test/spec/client.rb +248 -0
- data/test/spec/collection_proxy.rb +109 -0
- data/test/spec/json_serializer.rb +108 -0
- data/test/spec/metadata.rb +37 -0
- data/test/spec/property_metadata.rb +63 -0
- data/test/spec/proxy.rb +87 -0
- data/test/spec/proxy_factory.rb +44 -0
- data/test/spec/resource_metadata.rb +96 -0
- data/test/spec/test_helper.rb +28 -0
- metadata +173 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Rod
|
4
|
+
module Rest
|
5
|
+
class JsonSerializer
|
6
|
+
# Serialize given Rod +object+ to JSON.
|
7
|
+
# The serialized object looks as follows:
|
8
|
+
# {
|
9
|
+
# rod_id: 1, # required +rod_id+
|
10
|
+
# type: "Car", # required +type+
|
11
|
+
# name: "Mercedes 300", # field value
|
12
|
+
# owner: { rod_id: 1, type: "Person" } # singular association value
|
13
|
+
# drivers: { count: 3 } # plural association value
|
14
|
+
# }
|
15
|
+
def serialize(object)
|
16
|
+
if object.is_a?(Rod::Model)
|
17
|
+
serialize_rod_object(object)
|
18
|
+
elsif object.respond_to?(:each)
|
19
|
+
serialize_collection(object)
|
20
|
+
else
|
21
|
+
serialize_basic_value(object)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def serialize_rod_object(object)
|
27
|
+
build_object_hash(object).to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
def serialize_collection(collection)
|
31
|
+
collection.map{|o| build_object_hash(o) }.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize_basic_value(value)
|
35
|
+
value.to_json
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_object_hash(object)
|
39
|
+
result = { rod_id: object.rod_id, type: object.class.to_s }
|
40
|
+
resource = object.class
|
41
|
+
resource.fields.each do |field|
|
42
|
+
result[field.name] = object.send(field.name)
|
43
|
+
end
|
44
|
+
resource.singular_associations.each do |association|
|
45
|
+
associated = object.send(association.name)
|
46
|
+
if associated
|
47
|
+
result[association.name] = { rod_id: associated.rod_id, type: associated.class.to_s }
|
48
|
+
else
|
49
|
+
result[association.name] = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
resource.plural_associations.each do |association|
|
53
|
+
result[association.name] = { count: object.send(association.name).size }
|
54
|
+
end
|
55
|
+
result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Rod
|
2
|
+
module Rest
|
3
|
+
class Metadata
|
4
|
+
ROD_KEY = /\ARod\b/
|
5
|
+
|
6
|
+
attr_reader :description
|
7
|
+
|
8
|
+
# Initializes the metadata via the options:
|
9
|
+
# * description - text representation of the metadata
|
10
|
+
# * parser - parser used to parse the text representation of the metadata
|
11
|
+
# * resource_metadata_factory - factory used to create metadata for the
|
12
|
+
# resources
|
13
|
+
def initialize(options={})
|
14
|
+
@description = options.fetch(:description)
|
15
|
+
parser = options[:parser] || JSON
|
16
|
+
@resource_metadata_factory = options[:resource_metadata_factory] || ResourceMetadata
|
17
|
+
@resources = create_resource_descriptions(parser.parse(@description, symbolize_names: true))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return collection of resource metadata.
|
21
|
+
def resources
|
22
|
+
@resources
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def create_resource_descriptions(hash_description)
|
27
|
+
hash_description.map do |name,description|
|
28
|
+
next if restricted_name?(name)
|
29
|
+
@resource_metadata_factory.new(name,description)
|
30
|
+
end.compact
|
31
|
+
end
|
32
|
+
|
33
|
+
def restricted_name?(name)
|
34
|
+
name.to_s =~ ROD_KEY
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'active_model/naming'
|
2
|
+
|
3
|
+
module Rod
|
4
|
+
module Rest
|
5
|
+
module Naming
|
6
|
+
def plural_resource_name(resource)
|
7
|
+
singular_resource_name(resource).pluralize
|
8
|
+
end
|
9
|
+
|
10
|
+
def singular_resource_name(resource)
|
11
|
+
if resource.respond_to?(:name)
|
12
|
+
name = resource.name
|
13
|
+
else
|
14
|
+
name = resource.to_s
|
15
|
+
end
|
16
|
+
name.gsub("::","_").downcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rod
|
2
|
+
module Rest
|
3
|
+
class PropertyMetadata
|
4
|
+
attr_reader :name, :symbolic_name
|
5
|
+
|
6
|
+
# Creates new property metadata using the +name+ and +options+.
|
7
|
+
def initialize(name, options)
|
8
|
+
raise ArgumentError.new("nil name") if name.nil?
|
9
|
+
@name = name.to_s
|
10
|
+
@symbolic_name = @name.to_sym
|
11
|
+
@index = options[:index]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns true if the property is indexed.
|
15
|
+
def indexed?
|
16
|
+
!! @index
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'rod/rest/exception'
|
2
|
+
|
3
|
+
module Rod
|
4
|
+
module Rest
|
5
|
+
class Proxy
|
6
|
+
# Initialize new Proxy factory based on the +metadata+ and associated with
|
7
|
+
# the +client+, used to fetch the descriptions of the objects.
|
8
|
+
# Options:
|
9
|
+
# * collection_proxy_factory - factory used to create collection proxies
|
10
|
+
# for plural associations
|
11
|
+
def initialize(metadata,client,options={})
|
12
|
+
@metadata = metadata
|
13
|
+
@client = client
|
14
|
+
@type = @metadata.name
|
15
|
+
@collection_proxy_factory = options[:collection_proxy_factory] || CollectionProxy
|
16
|
+
@klass = build_class(@metadata)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return new instance of a proxy object based on the +hash+ data.
|
20
|
+
def new(hash)
|
21
|
+
check_id(hash)
|
22
|
+
proxy = @klass.new(hash[:rod_id],@type,@client,@collection_proxy_factory)
|
23
|
+
@metadata.fields.each do |field|
|
24
|
+
check_field(hash,field)
|
25
|
+
proxy.instance_variable_set("@#{field.symbolic_name}",hash[field.symbolic_name])
|
26
|
+
end
|
27
|
+
@metadata.singular_associations.each do |association|
|
28
|
+
check_association(hash,association)
|
29
|
+
proxy.instance_variable_set(association_variable_name(association),hash[association.symbolic_name])
|
30
|
+
end
|
31
|
+
@metadata.plural_associations.each do |association|
|
32
|
+
check_association(hash,association)
|
33
|
+
proxy.instance_variable_set(count_variable_name(association),hash[association.symbolic_name][:count])
|
34
|
+
end
|
35
|
+
proxy
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def build_class(metadata)
|
40
|
+
Class.new do
|
41
|
+
attr_reader :type,:rod_id
|
42
|
+
|
43
|
+
def initialize(rod_id,type,client,collection_proxy_factory)
|
44
|
+
@rod_id = rod_id
|
45
|
+
@type = type
|
46
|
+
@client = client
|
47
|
+
@collection_proxy_factory = collection_proxy_factory
|
48
|
+
end
|
49
|
+
|
50
|
+
metadata.fields.each do |field|
|
51
|
+
attr_reader field.symbolic_name
|
52
|
+
end
|
53
|
+
|
54
|
+
metadata.singular_associations.each do |association|
|
55
|
+
class_eval <<-END
|
56
|
+
def #{association.symbolic_name}
|
57
|
+
if defined?(@#{association.name})
|
58
|
+
return @#{association.name}
|
59
|
+
end
|
60
|
+
@#{association.name} = @client.fetch_object(@_#{association.name}_description)
|
61
|
+
end
|
62
|
+
END
|
63
|
+
end
|
64
|
+
|
65
|
+
metadata.plural_associations.each do |association|
|
66
|
+
class_eval <<-END
|
67
|
+
def #{association.name}
|
68
|
+
@collection_proxy_factory.new(self,"#{association.name}",@_#{association.name}_count,@client)
|
69
|
+
end
|
70
|
+
END
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_id(hash)
|
76
|
+
unless hash.has_key?(:rod_id)
|
77
|
+
raise InvalidData.new(missing_rod_id_message(hash))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_field(hash,field)
|
82
|
+
unless hash.has_key?(field.symbolic_name)
|
83
|
+
raise InvalidData.new(missing_field_error_message(field,hash))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_association(hash,association)
|
88
|
+
unless hash.has_key?(association.symbolic_name)
|
89
|
+
raise InvalidData.new(missing_association_error_message(association,hash))
|
90
|
+
end
|
91
|
+
if !hash[association.symbolic_name].nil? && ! Hash === hash[association.symbolic_name]
|
92
|
+
raise InvalidData.new(not_hash_error_message(association,hash[association.symbolic_name]))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def missing_rod_id_message(hash)
|
97
|
+
"The data doesn't have a rod_id #{hash}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def missing_field_error_message(field,hash)
|
101
|
+
"The field '#{field.symbolic_name}' is missing in the hash: #{hash}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def missing_association_error_message(association,hash)
|
105
|
+
"The association '#{association.symbolic_name}' is missing in the hash: #{hash}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def not_hash_error_message(association,value)
|
109
|
+
"The association '#{association.symbolic_name}' is not a hash: #{value}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def association_variable_name(association)
|
113
|
+
"@_#{association.symbolic_name}_description"
|
114
|
+
end
|
115
|
+
|
116
|
+
def count_variable_name(association)
|
117
|
+
"@_#{association.symbolic_name}_count"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rod/rest/exception'
|
2
|
+
|
3
|
+
module Rod
|
4
|
+
module Rest
|
5
|
+
class ProxyFactory
|
6
|
+
# Creates new proxy factory based on the +metadata+ and using given web
|
7
|
+
# +client+.
|
8
|
+
# Options:
|
9
|
+
# * proxy_class - the class used to create the resource proxy factories.
|
10
|
+
def initialize(metadata,client,options={})
|
11
|
+
proxy_class = options[:proxy_class] || Proxy
|
12
|
+
@proxies = {}
|
13
|
+
metadata.each do |resource_metadata|
|
14
|
+
@proxies[resource_metadata.name] = proxy_class.new(resource_metadata,client)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Build new object-proxy from the hash-like +object_description+.
|
19
|
+
def build(object_description)
|
20
|
+
check_type(object_description[:type])
|
21
|
+
@proxies[object_description[:type]].new(object_description)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def check_type(type)
|
26
|
+
raise UnknownResource.new(type) unless @proxies.has_key?(type)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rod
|
2
|
+
module Rest
|
3
|
+
class ResourceMetadata
|
4
|
+
attr_reader :name, :fields, :singular_associations, :plural_associations, :properties, :indexed_properties
|
5
|
+
|
6
|
+
# Create new resource metadata for a resource with +name+ and
|
7
|
+
# +description+. Options:
|
8
|
+
# * +property_factory+ - factory used to create descriptions of the
|
9
|
+
# properties
|
10
|
+
def initialize(name,description,options={})
|
11
|
+
@name = name.to_s
|
12
|
+
@property_factory = options[:property_factory] || PropertyMetadata
|
13
|
+
@fields = create_properties(description[:fields])
|
14
|
+
@singular_associations = create_properties(description[:has_one])
|
15
|
+
@plural_associations = create_properties(description[:has_many])
|
16
|
+
@properties = @fields + @singular_associations + @plural_associations
|
17
|
+
@indexed_properties = @properties.select{|p| p.indexed? }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def create_properties(description)
|
22
|
+
if description
|
23
|
+
description.map do |property_description|
|
24
|
+
@property_factory.new(*property_description)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/rod-rest.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$:.unshift "lib"
|
2
|
+
require 'rod/rest/constants'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "rod-rest"
|
6
|
+
s.version = Rod::Rest::VERSION
|
7
|
+
s.date = "#{Time.now.strftime("%Y-%m-%d")}"
|
8
|
+
s.required_ruby_version = '= 1.9.2'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ['Aleksander Pohl']
|
11
|
+
s.email = ["apohllo@o2.pl"]
|
12
|
+
s.homepage = "http://github.com/apohllo/rod-rest"
|
13
|
+
s.summary = "REST API for ROD"
|
14
|
+
s.description = "REST API for Ruby Object Database allows for talking to the DB via HTTP."
|
15
|
+
|
16
|
+
s.rubyforge_project = "rod-rest"
|
17
|
+
#s.rdoc_options = ["--main", "README.rdoc"]
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_path = "lib"
|
23
|
+
|
24
|
+
s.add_dependency("sinatra")
|
25
|
+
s.add_dependency("rod")
|
26
|
+
s.add_dependency("faraday")
|
27
|
+
|
28
|
+
s.add_development_dependency("rack-test")
|
29
|
+
s.add_development_dependency("rspec")
|
30
|
+
s.add_development_dependency("rr")
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'rod'
|
3
|
+
require 'rod/rest'
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
class Database < Rod::Database
|
7
|
+
end
|
8
|
+
|
9
|
+
class Model < Rod::Model
|
10
|
+
database_class Database
|
11
|
+
end
|
12
|
+
|
13
|
+
class Car < Model
|
14
|
+
field :brand, :string, index: :hash
|
15
|
+
has_one :owner, class_name: "Person"
|
16
|
+
has_many :drivers, class_name: "Person"
|
17
|
+
end
|
18
|
+
|
19
|
+
class Person < Model
|
20
|
+
field :name, :string, index: :hash
|
21
|
+
field :surname, :string, index: :hash
|
22
|
+
end
|
23
|
+
|
24
|
+
module Rod
|
25
|
+
module Rest
|
26
|
+
describe "end-to-end tests" do
|
27
|
+
def verify_person_equality(person1,person2)
|
28
|
+
person1.name.should == person2.name
|
29
|
+
person1.surname.should == person2.surname
|
30
|
+
end
|
31
|
+
|
32
|
+
PATH = "data/end_to_end"
|
33
|
+
SCHUMAHER_NAME = "Michael"
|
34
|
+
SCHUMAHER_SURNAME = "Schumaher"
|
35
|
+
KUBICA_NAME = "Robert"
|
36
|
+
KUBICA_SURNAME = "Kubica"
|
37
|
+
MERCEDES_300_NAME = "Mercedes 300"
|
38
|
+
|
39
|
+
before(:all) do
|
40
|
+
::Database.instance.create_database(PATH)
|
41
|
+
schumaher = Person.new(name: SCHUMAHER_NAME, surname: SCHUMAHER_SURNAME)
|
42
|
+
schumaher.store
|
43
|
+
kubica = Person.new(name: KUBICA_NAME, surname: KUBICA_SURNAME)
|
44
|
+
kubica.store
|
45
|
+
mercedes_300 = Car.new(brand: MERCEDES_300_NAME, owner: schumaher, drivers: [schumaher,kubica])
|
46
|
+
mercedes_300.store
|
47
|
+
audi_a4 = Car.new(brand: "Audi A4", owner: nil)
|
48
|
+
audi_a4.store
|
49
|
+
::Database.instance.close_database
|
50
|
+
end
|
51
|
+
|
52
|
+
after(:all) do
|
53
|
+
require 'fileutils'
|
54
|
+
FileUtils.rm_rf(PATH)
|
55
|
+
end
|
56
|
+
|
57
|
+
before do
|
58
|
+
::Database.instance.open_database(PATH)
|
59
|
+
end
|
60
|
+
|
61
|
+
after do
|
62
|
+
::Database.instance.close_database
|
63
|
+
end
|
64
|
+
|
65
|
+
example "Schumaher is in the DB" do
|
66
|
+
schumaher = Person.find_by_surname(SCHUMAHER_SURNAME)
|
67
|
+
schumaher.name.should == SCHUMAHER_NAME
|
68
|
+
end
|
69
|
+
|
70
|
+
example "Mercedes 300 is in the DB" do
|
71
|
+
mercedes_300 = Car.find_by_brand(MERCEDES_300_NAME)
|
72
|
+
mercedes_300.owner.should == Person.find_by_surname(SCHUMAHER_SURNAME)
|
73
|
+
mercedes_300.drivers[1].should == Person.find_by_surname(KUBICA_SURNAME)
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "with REST API and client" do
|
77
|
+
let(:client) { Client.new(http_client: http_client) }
|
78
|
+
let(:http_client) { Faraday.new(url: "http://localhost:4567") }
|
79
|
+
|
80
|
+
before(:all) do
|
81
|
+
Thread.new { API.start_with_database(::Database.instance,{},logging: nil) }
|
82
|
+
sleep 0.5
|
83
|
+
end
|
84
|
+
|
85
|
+
after(:all) do
|
86
|
+
#Thread.join
|
87
|
+
end
|
88
|
+
|
89
|
+
example "API serves the metadata" do
|
90
|
+
client.metadata.resources.size.should == 3
|
91
|
+
person = client.metadata.resources.find{|r| r.name == "Person" }
|
92
|
+
person.fields.size == 2
|
93
|
+
person.fields.zip(%w{name surname}).each do |field,field_name|
|
94
|
+
field.name.should == field_name
|
95
|
+
end
|
96
|
+
car = client.metadata.resources.find{|r| r.name == "Car" }
|
97
|
+
car.fields.zip(%w{brand}).each do |field,field_name|
|
98
|
+
field.name.should == field_name
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
example "Schumaher might be retrieved by id" do
|
103
|
+
schumaher_in_rod = Person.find_by_name(SCHUMAHER_NAME)
|
104
|
+
schumaher_via_api = client.find_person(schumaher_in_rod.rod_id)
|
105
|
+
verify_person_equality(schumaher_via_api,schumaher_in_rod)
|
106
|
+
end
|
107
|
+
|
108
|
+
example "Kubica might be retrieved by name" do
|
109
|
+
kubica_in_rod = Person.find_by_name(KUBICA_NAME)
|
110
|
+
kubica_via_api = client.find_people_by_name(KUBICA_NAME).first
|
111
|
+
verify_person_equality(kubica_via_api,kubica_in_rod)
|
112
|
+
end
|
113
|
+
|
114
|
+
example "Mercedes might be retrieved by id" do
|
115
|
+
mercedes_in_rod = Car.find_by_brand(MERCEDES_300_NAME)
|
116
|
+
mercedes_via_api = client.find_car(mercedes_in_rod.rod_id)
|
117
|
+
mercedes_via_api.brand.should == mercedes_in_rod.brand
|
118
|
+
end
|
119
|
+
|
120
|
+
example "Mercedes owner is retrieved properly" do
|
121
|
+
mercedes_in_rod = Car.find_by_brand(MERCEDES_300_NAME)
|
122
|
+
schumaher_in_rod = Person.find_by_name(SCHUMAHER_NAME)
|
123
|
+
mercedes_via_api = client.find_car(mercedes_in_rod.rod_id)
|
124
|
+
verify_person_equality(mercedes_via_api.owner,schumaher_in_rod)
|
125
|
+
end
|
126
|
+
|
127
|
+
example "Mercedes drivers are retrieved properly" do
|
128
|
+
mercedes_in_rod = Car.find_by_brand(MERCEDES_300_NAME)
|
129
|
+
schumaher_in_rod = Person.find_by_name(SCHUMAHER_NAME)
|
130
|
+
kubica_in_rod = Person.find_by_name(KUBICA_NAME)
|
131
|
+
mercedes_via_api = client.find_car(mercedes_in_rod.rod_id)
|
132
|
+
verify_person_equality(mercedes_via_api.drivers[0],schumaher_in_rod)
|
133
|
+
verify_person_equality(mercedes_via_api.drivers[1],kubica_in_rod)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|