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.
@@ -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