rod-rest 0.0.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/.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
|