grom 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +48 -13
- data/lib/grom/base.rb +38 -52
- data/lib/grom/graph_mapper.rb +34 -57
- data/lib/grom/helpers.rb +17 -19
- data/lib/grom.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34878dcc73dda4ed7d3145cc1bee503d8843d471
|
4
|
+
data.tar.gz: 91303eb86ec021e9ef6d4ab62edb271c1d8a4b89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 362e2f954adc564f22875118b2ee2901a968183a72ba900da4d7469153b1198c2f2793efa22c88f0a88bd7b25aa22f91ef58f983124dd5ad6298429114e620b9
|
7
|
+
data.tar.gz: 74f6dd9f20628e1e3e766ca1874c487ea4368aabbf99c33bbe647f1ed488a379216368c75e364d995d4a7b13d4732ba701925ec3f5add0af94534367a977ef29
|
data/README.md
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
# Graph Object Mapper (Grom)
|
2
|
-
Grom is a custom ORM that takes graph data and deserializes it into objects. It uses the ruby-rdf library.
|
3
|
-
of UK Parliament new website.
|
2
|
+
Grom is a custom ORM that takes graph data and deserializes it into objects. It uses the [RDF.rb](https://github.com/ruby-rdf/rdf) library. It was created in the context
|
3
|
+
of UK Parliament's new website.
|
4
4
|
|
5
5
|
## Installation
|
6
|
+
```apple js
|
7
|
+
gem install grom
|
8
|
+
```
|
6
9
|
Add this line to your application's Gemfile:
|
7
10
|
|
8
11
|
```ruby
|
9
|
-
gem 'grom'
|
12
|
+
gem 'grom'
|
10
13
|
```
|
11
14
|
|
12
|
-
|
15
|
+
Then execute:
|
13
16
|
```bash
|
14
17
|
$ bundle
|
15
18
|
```
|
@@ -17,7 +20,7 @@ $ bundle
|
|
17
20
|
## Usage
|
18
21
|
|
19
22
|
#### Set up
|
20
|
-
You will need to set three constants in order for the
|
23
|
+
You will need to set three constants in order for the gem to work.
|
21
24
|
|
22
25
|
```
|
23
26
|
API_ENDPOINT = 'http://example.com'
|
@@ -25,14 +28,14 @@ API_ENDPOINT_HOST = 'example.com'
|
|
25
28
|
DATA_URI_PREFIX = 'http://id.example.com'
|
26
29
|
```
|
27
30
|
|
28
|
-
|
29
|
-
|
31
|
+
The API Endpoint will be the endpoint where you are making the calls to get graph data. <br>
|
32
|
+
The Data URI prefix is the prefix that you use to define the entities in your triple store.
|
30
33
|
|
31
34
|
|
32
35
|
#### Models
|
33
|
-
|
36
|
+
Models inherit from `Grom::Base` and need a `self.property_translator` hash defined. The keys are predicates (without the prefix) and the values are the translated object properties.
|
34
37
|
|
35
|
-
|
38
|
+
An example of a Person class.
|
36
39
|
|
37
40
|
```
|
38
41
|
class Person < Grom::Base
|
@@ -49,10 +52,31 @@ class Person < Grom::Base
|
|
49
52
|
end
|
50
53
|
```
|
51
54
|
|
55
|
+
#### Retrieving Objects
|
56
|
+
|
57
|
+
##### #all
|
58
|
+
To return a collection of objects use the class method `all`.
|
59
|
+
```apple js
|
60
|
+
people = Person.all #=> returns an array of People objects
|
61
|
+
```
|
62
|
+
|
63
|
+
This method can take unlimited parameters. Including a parameter appends it to the endpoint url creating a new url. For example,
|
64
|
+
|
65
|
+
```apple js
|
66
|
+
people = Person.all('current') #=> returns an array of current People
|
67
|
+
```
|
68
|
+
This generates the endpoint url `http://#{API_ENDPOINT}/people/current.ttl` and returns a list of current people from there.
|
69
|
+
|
70
|
+
##### #find
|
71
|
+
To return a single object use the class method `find` with an id.
|
72
|
+
```apple js
|
73
|
+
person = Person.find('1') #=> returns a Person object
|
74
|
+
```
|
75
|
+
|
52
76
|
#### Associations
|
53
77
|
|
54
78
|
##### #has_many
|
55
|
-
For a simple `has_many` association,
|
79
|
+
For a simple `has_many` association, declare the association at the top of the model. For example, if `Person` has many `contact_points`, create a `ContactPoint` class and declare the association at the top of the `Person` class.
|
56
80
|
|
57
81
|
The name of the association must match the name of the class but it needs to be underscored and pluralized just like in ActiveRecord.
|
58
82
|
|
@@ -61,17 +85,28 @@ class Person < Grom::Base
|
|
61
85
|
has_many :contact_points
|
62
86
|
```
|
63
87
|
|
88
|
+
It is then possible to retrieve the contact_points for a person in the following way:
|
89
|
+
|
90
|
+
```apple js
|
91
|
+
person = Person.find('1')
|
92
|
+
person.contact_points #=> returns an array of contact_points related to person
|
93
|
+
```
|
64
94
|
|
65
95
|
##### #has_many_through
|
66
|
-
For a `has_many_through` association,
|
96
|
+
For a `has_many_through` association, declare the association at the top of the model. For example, if `Person` has many `parties` through `party_memberships`, create a `Party` class and a `PartyMembership` class, then declare the association at the top of the `Person` class.
|
67
97
|
|
68
98
|
```
|
69
99
|
class Person < Grom::Base
|
70
100
|
has_many_through :parties, via: :party_memberships
|
71
101
|
```
|
72
102
|
|
73
|
-
|
74
|
-
|
103
|
+
It is then possible to retrieve the parties for a person in the following way:
|
104
|
+
|
105
|
+
```apple js
|
106
|
+
person = Person.find('1')
|
107
|
+
person.parties #=> returns an array of parties with the party_memberships stored as a hash
|
108
|
+
```
|
109
|
+
|
75
110
|
|
76
111
|
## License
|
77
112
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/grom/base.rb
CHANGED
@@ -8,14 +8,10 @@ module Grom
|
|
8
8
|
extend Grom::Helpers
|
9
9
|
|
10
10
|
def initialize(attributes)
|
11
|
-
unless attributes == {}
|
12
|
-
ttl_graph = self.class.convert_to_ttl(attributes[:graph]).gsub("'", "\\\\'")
|
13
|
-
self.instance_eval("def graph; self.class.create_graph_from_ttl('#{ttl_graph}') ; end")
|
14
|
-
end
|
15
11
|
attributes.each do |k, v|
|
16
12
|
translated_key = self.class.property_translator[k]
|
17
13
|
v = self.class.create_property_name(self.class.get_id(v)) if (v =~ URI::regexp) == 0
|
18
|
-
unless
|
14
|
+
unless v.nil? || translated_key.nil?
|
19
15
|
instance_variable_set("@#{translated_key}", v)
|
20
16
|
self.class.send(:attr_reader, translated_key)
|
21
17
|
end
|
@@ -24,54 +20,40 @@ module Grom
|
|
24
20
|
|
25
21
|
def self.find(id)
|
26
22
|
endpoint_url = "#{find_base_url_builder(self.name, id)}.ttl"
|
27
|
-
|
28
|
-
self.object_single_maker(
|
23
|
+
ttl_data = get_ttl_data(endpoint_url)
|
24
|
+
self.object_single_maker(ttl_data)
|
29
25
|
end
|
30
26
|
|
31
27
|
def self.all(*options)
|
32
28
|
endpoint_url = "#{all_base_url_builder(self.name, *options)}.ttl"
|
33
|
-
|
34
|
-
|
29
|
+
ttl_data = get_ttl_data(endpoint_url)
|
30
|
+
|
31
|
+
self.object_array_maker(ttl_data)
|
35
32
|
end
|
36
33
|
|
37
34
|
def self.has_many(association)
|
38
|
-
self.class_eval("def #{association}(
|
35
|
+
self.class_eval("def #{association}(*options); #{create_class_name(association)}.has_many_query(self, *options); end")
|
39
36
|
end
|
40
37
|
|
41
38
|
def self.has_many_through(association, through_association)
|
42
39
|
self.has_many(through_association[:via])
|
43
|
-
self.class_eval("def #{association}(
|
40
|
+
self.class_eval("def #{association}(*options); #{create_class_name(association)}.has_many_through_query(self, #{create_class_name(through_association[:via])}.new({}).class.name, *options); end")
|
44
41
|
end
|
45
42
|
|
46
43
|
def self.has_one(association)
|
47
|
-
self.class_eval("def #{association}(
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.has_many_query(owner_object, optional=nil)
|
51
|
-
endpoint_url = associations_url_builder(owner_object, self.name, {optional: optional })
|
52
|
-
graph_data = get_graph_data(endpoint_url)
|
53
|
-
self.object_array_maker(graph_data)
|
44
|
+
self.class_eval("def #{association}(*options); #{create_class_name(association)}.has_one_query(self, *options); end")
|
54
45
|
end
|
55
46
|
|
56
|
-
def self.
|
57
|
-
endpoint_url = associations_url_builder(owner_object, self.name, {optional:
|
58
|
-
|
59
|
-
self.
|
47
|
+
def self.has_many_query(owner_object, *options)
|
48
|
+
endpoint_url = associations_url_builder(owner_object, self.name, {optional: options })
|
49
|
+
ttl_data = get_ttl_data(endpoint_url)
|
50
|
+
self.object_array_maker(ttl_data)
|
60
51
|
end
|
61
52
|
|
62
|
-
def self.
|
63
|
-
endpoint_url = associations_url_builder(owner_object, self.name, {optional:
|
64
|
-
|
65
|
-
|
66
|
-
associated_objects_array = self.object_array_maker(separated_graphs[:associated_class_graph])
|
67
|
-
through_property_plural = create_plural_property_name(through_class)
|
68
|
-
self.through_getter_setter(through_property_plural)
|
69
|
-
associated_objects_array.each do |associated_object|
|
70
|
-
through_class_array = get_through_graphs(separated_graphs[:through_graph], associated_object.id).map do |graph|
|
71
|
-
ActiveSupport::Inflector.constantize(through_class).object_single_maker(graph)
|
72
|
-
end
|
73
|
-
associated_object.send((through_property_plural + '=').to_sym, through_class_array)
|
74
|
-
end
|
53
|
+
def self.has_one_query(owner_object, *options)
|
54
|
+
endpoint_url = associations_url_builder(owner_object, self.name, {optional: options, single: true })
|
55
|
+
ttl_data = get_ttl_data(endpoint_url)
|
56
|
+
self.object_single_maker(ttl_data)
|
75
57
|
end
|
76
58
|
|
77
59
|
def self.through_getter_setter(through_property_plural)
|
@@ -79,30 +61,34 @@ module Grom
|
|
79
61
|
self.class_eval("def #{through_property_plural}; @#{through_property_plural}; end")
|
80
62
|
end
|
81
63
|
|
82
|
-
def self.object_array_maker(
|
83
|
-
|
84
|
-
self.new(
|
64
|
+
def self.object_array_maker(ttl_data)
|
65
|
+
create_hash_from_ttl(ttl_data).map do |hash|
|
66
|
+
self.new(hash)
|
85
67
|
end
|
86
68
|
end
|
87
69
|
|
88
|
-
def self.object_single_maker(
|
89
|
-
self.object_array_maker(
|
70
|
+
def self.object_single_maker(ttl_data)
|
71
|
+
self.object_array_maker(ttl_data).first
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.has_many_through_query(owner_object, through_class, *options)
|
75
|
+
through_property_plural = create_plural_property_name(through_class)
|
76
|
+
endpoint_url = associations_url_builder(owner_object, self.name, {optional: options })
|
77
|
+
self.through_getter_setter(through_property_plural)
|
78
|
+
ttl_data = get_ttl_data(endpoint_url)
|
79
|
+
self.map_hashes_to_objects(through_split_graph(ttl_data), through_property_plural)
|
90
80
|
end
|
91
81
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
if association.is_a?(Hash)
|
99
|
-
association_name = association.keys.first
|
100
|
-
hash[association_name] = self.send(association_name, association.values.first)
|
101
|
-
else
|
102
|
-
hash[association] = self.send(association)
|
82
|
+
def self.map_hashes_to_objects(hashes, through_property_plural)
|
83
|
+
through_array = hashes[:through_class_hash].values
|
84
|
+
hashes[:associated_class_hash].values.map do |hash|
|
85
|
+
associated_object = self.new(hash)
|
86
|
+
through_obj_array, through_array = through_array.partition do |t_hash|
|
87
|
+
t_hash[:associated_object_id] == associated_object.id
|
103
88
|
end
|
89
|
+
associated_object.send((through_property_plural + '=').to_sym, through_obj_array)
|
90
|
+
associated_object
|
104
91
|
end
|
105
|
-
hash
|
106
92
|
end
|
107
93
|
end
|
108
94
|
end
|
data/lib/grom/graph_mapper.rb
CHANGED
@@ -1,79 +1,56 @@
|
|
1
1
|
require 'grom'
|
2
|
+
require 'rdf/turtle'
|
2
3
|
|
3
4
|
module Grom
|
4
5
|
module GraphMapper
|
5
6
|
|
6
|
-
def
|
7
|
-
|
8
|
-
create_graph_from_ttl(ttl_data)
|
7
|
+
def get_ttl_data(uri)
|
8
|
+
Net::HTTP.get(URI(uri))
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
result = ""
|
14
|
-
data.each_statement do |statement|
|
15
|
-
result << RDF::NTriples::Writer.serialize(statement)
|
16
|
-
end
|
17
|
-
result
|
11
|
+
def get_id(uri)
|
12
|
+
uri == RDF.type.to_s ? 'type' : uri.to_s.split("/").last
|
18
13
|
end
|
19
14
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
ttl_data
|
24
|
-
graph = RDF::Graph.new
|
25
|
-
RDF::NTriples::Reader.new(ttl_data) do |reader|
|
15
|
+
def create_hash_from_ttl(ttl_data)
|
16
|
+
hash = {}
|
17
|
+
RDF::Turtle::Reader.new(ttl_data) do |reader|
|
26
18
|
reader.each_statement do |statement|
|
27
|
-
|
19
|
+
statement_mapper(statement, hash)
|
28
20
|
end
|
29
21
|
end
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def get_id(uri)
|
34
|
-
uri == RDF.type.to_s ? 'type' : uri.to_s.split("/").last
|
22
|
+
hash.values
|
35
23
|
end
|
36
24
|
|
37
|
-
def
|
38
|
-
|
39
|
-
{
|
25
|
+
def statement_mapper(statement, hash)
|
26
|
+
subject = get_id(statement.subject)
|
27
|
+
hash[subject] ||= { :id => subject }
|
28
|
+
hash[subject][get_id(statement.predicate).to_sym] = statement.object.to_s
|
40
29
|
end
|
41
30
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
subject_pattern = RDF::Query::Pattern.new(subject, :predicate, :object)
|
61
|
-
graph.query(subject_pattern).each do |statement|
|
62
|
-
associated_graph << statement
|
31
|
+
def through_split_graph(ttl_data)
|
32
|
+
associated_hash, through_hash = {}, {}
|
33
|
+
RDF::Turtle::Reader.new(ttl_data) do |reader|
|
34
|
+
reader.each_statement do |s|
|
35
|
+
if (s.subject.to_s =~ URI::regexp) == 0
|
36
|
+
subject = get_id(s.subject)
|
37
|
+
associated_hash[subject] ||= { :id => subject }
|
38
|
+
associated_hash[subject][get_id(s.predicate).to_sym] = s.object.to_s
|
39
|
+
else
|
40
|
+
through_hash[s.subject.to_s] ||= {}
|
41
|
+
if get_id(s.predicate) == "connect"
|
42
|
+
through_hash[s.subject.to_s][:associated_object_id] = get_id(s.object)
|
43
|
+
elsif get_id(s.predicate) == "objectId"
|
44
|
+
through_hash[s.subject.to_s][:id] = get_id(s.object)
|
45
|
+
else
|
46
|
+
through_hash[s.subject.to_s][get_id(s.predicate).to_sym] = s.object.to_s
|
47
|
+
end
|
48
|
+
end
|
63
49
|
end
|
64
|
-
through_graph << graph
|
65
|
-
through_graph.delete(associated_graph)
|
66
50
|
end
|
67
|
-
{ through_graph: through_graph, associated_class_graph: associated_graph }
|
68
|
-
end
|
69
51
|
|
70
|
-
|
71
|
-
|
72
|
-
graph.query(connection_pattern).subjects.map do |subject|
|
73
|
-
subject_pattern = RDF::Query::Pattern.new(subject, :predicate, :object)
|
74
|
-
graph.query(subject_pattern)
|
75
|
-
end
|
76
|
-
end
|
52
|
+
{ associated_class_hash: associated_hash, through_class_hash: through_hash }
|
53
|
+
end
|
77
54
|
|
78
55
|
end
|
79
56
|
end
|
data/lib/grom/helpers.rb
CHANGED
@@ -7,7 +7,15 @@ module Grom
|
|
7
7
|
id = owner_object.id
|
8
8
|
associated_class_name = options[:single].nil? ? create_plural_property_name(associated_class_name) : create_property_name(associated_class_name)
|
9
9
|
endpoint = "#{find_base_url_builder(owner_object.class.name, id)}/#{associated_class_name}"
|
10
|
-
|
10
|
+
if options[:optional].nil?
|
11
|
+
endpoint += '.ttl'
|
12
|
+
else
|
13
|
+
options[:optional].each do |option|
|
14
|
+
endpoint += "/#{option}" unless option.nil?
|
15
|
+
end
|
16
|
+
endpoint += '.ttl'
|
17
|
+
end
|
18
|
+
endpoint
|
11
19
|
end
|
12
20
|
|
13
21
|
def find_base_url_builder(class_name, id)
|
@@ -34,27 +42,17 @@ module Grom
|
|
34
42
|
ActiveSupport::Inflector.underscore(class_name).downcase
|
35
43
|
end
|
36
44
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
45
|
+
def order_list(arr, *parameters)
|
46
|
+
rejected = []
|
47
|
+
arr.delete_if{ |obj| rejected << obj if parameters.any?{ |param| obj.send(param).nil? } }
|
48
|
+
sorted_arr = arr.sort_by do |obj|
|
49
|
+
parameters.map{ |param| obj.send(param) }.select{ |p| not p.nil? }
|
41
50
|
end
|
42
|
-
|
51
|
+
rejected.concat(sorted_arr)
|
43
52
|
end
|
44
53
|
|
45
|
-
def
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
def collective_through_graph(owner, association, through_property)
|
50
|
-
graph = owner.graph
|
51
|
-
association.each do |associated_object|
|
52
|
-
graph << associated_object.graph
|
53
|
-
associated_object.send(through_property).each do |through_object|
|
54
|
-
graph << through_object.graph
|
55
|
-
end
|
56
|
-
end
|
57
|
-
graph
|
54
|
+
def order_list_by_through(arr, through_association, property)
|
55
|
+
arr.map{ |obj| obj.send(through_association) }.flatten.sort{ |a, b| a[property] <=> b[property] }
|
58
56
|
end
|
59
57
|
end
|
60
58
|
end
|
data/lib/grom.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rebecca Appleyard
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-12-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -46,13 +46,13 @@ dependencies:
|
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
|
-
name:
|
49
|
+
name: rdf-turtle
|
50
50
|
requirement: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
type: :
|
55
|
+
type: :runtime
|
56
56
|
prerelease: false
|
57
57
|
version_requirements: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
@@ -60,19 +60,19 @@ dependencies:
|
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
63
|
+
name: rspec-rails
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: '0'
|
69
69
|
type: :development
|
70
70
|
prerelease: false
|
71
71
|
version_requirements: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: '0'
|
76
76
|
- !ruby/object:Gem::Dependency
|
77
77
|
name: webmock
|
78
78
|
requirement: !ruby/object:Gem::Requirement
|