rod-rest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Rod
2
+ module Rest
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ module Rod
2
+ module Rest
3
+ class MissingResource < RuntimeError; end
4
+ class APIError < RuntimeError; end
5
+ class InvalidData < RuntimeError; end
6
+ class UnknownResource < RuntimeError; end
7
+ end
8
+ end
@@ -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
@@ -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