ansr_dpla 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +137 -0
- data/README.md +59 -0
- data/ansr_dpla.gemspec +27 -0
- data/app/models/collection.rb +2 -0
- data/app/models/item.rb +2 -0
- data/fixtures/collection.json +1 -0
- data/fixtures/collection.jsonld +14 -0
- data/fixtures/collections.json +1 -0
- data/fixtures/collections.jsonld +65 -0
- data/fixtures/dpla.yml +2 -0
- data/fixtures/empty.jsonld +1 -0
- data/fixtures/item.json +1 -0
- data/fixtures/item.jsonld +272 -0
- data/fixtures/kittens.json +1 -0
- data/fixtures/kittens.jsonld +2477 -0
- data/fixtures/kittens_faceted.json +1 -0
- data/fixtures/kittens_faceted.jsonld +2693 -0
- data/lib/ansr_dpla.rb +9 -0
- data/lib/ansr_dpla/api.rb +78 -0
- data/lib/ansr_dpla/arel.rb +7 -0
- data/lib/ansr_dpla/arel/big_table.rb +108 -0
- data/lib/ansr_dpla/arel/visitors.rb +6 -0
- data/lib/ansr_dpla/arel/visitors/query_builder.rb +188 -0
- data/lib/ansr_dpla/arel/visitors/to_no_sql.rb +9 -0
- data/lib/ansr_dpla/connection_adapters/no_sql_adapter.rb +58 -0
- data/lib/ansr_dpla/model.rb +7 -0
- data/lib/ansr_dpla/model/base.rb +24 -0
- data/lib/ansr_dpla/model/pseudo_associate.rb +14 -0
- data/lib/ansr_dpla/model/querying.rb +34 -0
- data/lib/ansr_dpla/relation.rb +60 -0
- data/lib/ansr_dpla/request.rb +5 -0
- data/spec/adpla_test_api.rb +9 -0
- data/spec/lib/api_spec.rb +113 -0
- data/spec/lib/item_spec.rb +57 -0
- data/spec/lib/relation/facet_spec.rb +82 -0
- data/spec/lib/relation/select_spec.rb +54 -0
- data/spec/lib/relation/where_spec.rb +74 -0
- data/spec/lib/relation_spec.rb +307 -0
- data/spec/spec_helper.rb +36 -0
- data/test/debug.rb +14 -0
- data/test/system.rb +52 -0
- metadata +236 -0
data/lib/ansr_dpla.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
module Ansr::Dpla
|
3
|
+
class Api
|
4
|
+
include Ansr::Configurable
|
5
|
+
|
6
|
+
def config &block
|
7
|
+
super &block
|
8
|
+
raise "DPLA clients must be configured with an API key" unless @config[:api_key]
|
9
|
+
@config
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
API_PARAM_KEYS = [:api_key, :callback, :facets, :fields, :page, :page_size, :sort_by, :sort_by_pin, :sort_order]
|
14
|
+
|
15
|
+
def initialize(config=nil)
|
16
|
+
self.config{|x| x.merge!(config)} if config
|
17
|
+
end
|
18
|
+
|
19
|
+
def api_key
|
20
|
+
config[:api_key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def url
|
24
|
+
config[:url] || 'http://api.dp.la/v2/'
|
25
|
+
end
|
26
|
+
|
27
|
+
def path_for base, options = nil
|
28
|
+
return "#{base}?api_key=#{self.api_key}" unless options.is_a? Hash
|
29
|
+
options = {:api_key=>api_key}.merge(options)
|
30
|
+
API_PARAM_KEYS.each do |query_key|
|
31
|
+
options[query_key] = options[query_key].join(',') if options[query_key].is_a? Array
|
32
|
+
end
|
33
|
+
(options.keys - API_PARAM_KEYS).each do |query_key|
|
34
|
+
options[query_key] = options[query_key].join(' AND ') if options[query_key].is_a? Array
|
35
|
+
options[query_key].sub!(/^OR /,'')
|
36
|
+
options[query_key].gsub!(/\s+AND\sOR\s+/, ' OR ')
|
37
|
+
end
|
38
|
+
"#{base}" + (("?#{options.map { |key, value| "#{CGI::escape(key.to_s)}=#{CGI::escape(value.to_s)}"}.join("&") }" if options and not options.empty?) || '')
|
39
|
+
end
|
40
|
+
|
41
|
+
def client
|
42
|
+
@client ||= RestClient::Resource.new(self.url)
|
43
|
+
end
|
44
|
+
|
45
|
+
def items_path(options={})
|
46
|
+
path_for('items', options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def items(options = {})
|
50
|
+
client[items_path(options)].get
|
51
|
+
end
|
52
|
+
|
53
|
+
def item_path(id)
|
54
|
+
path_for("items/#{id}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def item(id)
|
58
|
+
client[item_path(id)].get
|
59
|
+
end
|
60
|
+
|
61
|
+
def collections_path(options={})
|
62
|
+
path_for('collections', options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def collections(options = {})
|
66
|
+
client[collections_path(options)].get
|
67
|
+
end
|
68
|
+
|
69
|
+
def collection_path(id)
|
70
|
+
path_for("collections/#{id}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def collection(id)
|
74
|
+
client[collection_path(id)].get
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Ansr::Dpla
|
2
|
+
module Arel
|
3
|
+
class BigTable < Ansr::Arel::BigTable
|
4
|
+
|
5
|
+
FIELDS = [
|
6
|
+
# can we list the fields from the DPLA v2 api?
|
7
|
+
# the sourceResource, originalRecord, and provider fields need to be associations, right?
|
8
|
+
:"_id",
|
9
|
+
:"dataProvider",
|
10
|
+
:"sourceResource",
|
11
|
+
:"object",
|
12
|
+
:"ingestDate",
|
13
|
+
:"originalRecord",
|
14
|
+
:"ingestionSequence",
|
15
|
+
:"isShownAt",
|
16
|
+
:"hasView",
|
17
|
+
:"provider",
|
18
|
+
:"@context",
|
19
|
+
:"ingestType",
|
20
|
+
:"@id",
|
21
|
+
:"id"
|
22
|
+
]
|
23
|
+
|
24
|
+
FACETS = [
|
25
|
+
:"sourceResource.contributor",
|
26
|
+
:"sourceResource.date.begin",
|
27
|
+
:"sourceResource.date.end",
|
28
|
+
:"sourceResource.language.name",
|
29
|
+
:"sourceResource.language.iso639",
|
30
|
+
:"sourceResource.format",
|
31
|
+
:"sourceResource.stateLocatedIn.name",
|
32
|
+
:"sourceResource.stateLocatedIn.iso3166-2",
|
33
|
+
:"sourceResource.spatial.name",
|
34
|
+
:"sourceResource.spatial.country",
|
35
|
+
:"sourceResource.spatial.region",
|
36
|
+
:"sourceResource.spatial.county",
|
37
|
+
:"sourceResource.spatial.state",
|
38
|
+
:"sourceResource.spatial.city",
|
39
|
+
:"sourceResource.spatial.iso3166-2",
|
40
|
+
:"sourceResource.spatial.coordinates",
|
41
|
+
:"sourceResource.subject.@id",
|
42
|
+
:"sourceResource.subject.name",
|
43
|
+
:"sourceResource.temporal.begin",
|
44
|
+
:"sourceResource.temporal.end",
|
45
|
+
:"sourceResource.type",
|
46
|
+
:"hasView.@id",
|
47
|
+
:"hasView.format",
|
48
|
+
:"isPartOf.@id",
|
49
|
+
:"isPartOf.name",
|
50
|
+
:"isShownAt",
|
51
|
+
:"object",
|
52
|
+
:"provider.@id",
|
53
|
+
:"provider.name",
|
54
|
+
]
|
55
|
+
|
56
|
+
SORTS = [
|
57
|
+
:"id",
|
58
|
+
:"@id",
|
59
|
+
:"sourceResource.id",
|
60
|
+
:"sourceResource.contributor",
|
61
|
+
:"sourceResource.date.begin",
|
62
|
+
:"sourceResource.date.end",
|
63
|
+
:"sourceResource.extent",
|
64
|
+
:"sourceResource.language.name",
|
65
|
+
:"sourceResource.language.iso639",
|
66
|
+
:"sourceResource.format",
|
67
|
+
:"sourceResource.stateLocatedIn.name",
|
68
|
+
:"sourceResource.stateLocatedIn.iso3166-2",
|
69
|
+
:"sourceResource.spatial.name",
|
70
|
+
:"sourceResource.spatial.country",
|
71
|
+
:"sourceResource.spatial.region",
|
72
|
+
:"sourceResource.spatial.county",
|
73
|
+
:"sourceResource.spatial.state",
|
74
|
+
:"sourceResource.spatial.city",
|
75
|
+
:"sourceResource.spatial.iso3166-2",
|
76
|
+
:"sourceResource.spatial.coordinates",
|
77
|
+
:"sourceResource.subject.@id",
|
78
|
+
:"sourceResource.subject.type",
|
79
|
+
:"sourceResource.subject.name",
|
80
|
+
:"sourceResource.temporal.begin",
|
81
|
+
:"sourceResource.temporal.end",
|
82
|
+
:"sourceResource.title",
|
83
|
+
:"sourceResource.type",
|
84
|
+
:"hasView.@id",
|
85
|
+
:"hasView.format",
|
86
|
+
:"isPartOf.@id",
|
87
|
+
:"isPartOf.name",
|
88
|
+
:"isShownAt",
|
89
|
+
:"object",
|
90
|
+
:"provider.@id",
|
91
|
+
:"provider.name",
|
92
|
+
]
|
93
|
+
|
94
|
+
def initialize(klass, opts={})
|
95
|
+
super(klass.model())
|
96
|
+
self.name = klass.name.downcase.pluralize
|
97
|
+
@fields += (opts[:fields] || FIELDS)
|
98
|
+
@facets += (opts[:facets] || FACETS)
|
99
|
+
@sorts += (opts[:sorts] || SORTS)
|
100
|
+
self.config(opts[:config]) if opts[:config]
|
101
|
+
end
|
102
|
+
|
103
|
+
def name
|
104
|
+
super.pluralize.downcase
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Ansr::Dpla::Arel::Visitors
|
2
|
+
class QueryBuilder < Ansr::Arel::Visitors::QueryBuilder
|
3
|
+
attr_reader :query_opts
|
4
|
+
|
5
|
+
def initialize(table, query_opts=nil)
|
6
|
+
super(table)
|
7
|
+
@query_opts = query_opts ||= Ansr::Dpla::Request.new
|
8
|
+
@query_opts.path = table.name
|
9
|
+
end
|
10
|
+
|
11
|
+
# determines whether multiple values should accumulate or overwrite in merges
|
12
|
+
def multiple?(field_key)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_String o, a
|
17
|
+
case a
|
18
|
+
when Ansr::Arel::Visitors::From
|
19
|
+
query_opts.path = o
|
20
|
+
when Ansr::Arel::Visitors::Facet
|
21
|
+
filter_field(o.to_sym)
|
22
|
+
when Ansr::Arel::Visitors::Order
|
23
|
+
order(o)
|
24
|
+
else
|
25
|
+
raise "visited String \"#{o}\" with #{a.to_s}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_Arel_SqlLiteral(n, attribute)
|
30
|
+
select_val = n.to_s.split(" AS ")
|
31
|
+
case attribute
|
32
|
+
when Ansr::Arel::Visitors::Order
|
33
|
+
order(n.to_s)
|
34
|
+
when Ansr::Arel::Visitors::Facet
|
35
|
+
filter_field(select_val[0].to_sym)
|
36
|
+
else
|
37
|
+
field(select_val[0].to_sym)
|
38
|
+
if select_val[1]
|
39
|
+
query_opts.aliases ||= {}
|
40
|
+
query_opts.aliases[select_val[0]] = select_val[1]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_Ansr_Arel_Nodes_Filter(object, attribute)
|
46
|
+
expr = object.expr
|
47
|
+
case expr
|
48
|
+
when ::Arel::SqlLiteral
|
49
|
+
visit expr, Ansr::Arel::Visitors::Filter.new(attribute) if object.select
|
50
|
+
when ::Arel::Attributes::Attribute
|
51
|
+
name = object.expr.name
|
52
|
+
name = "#{expr.relation.name}.#{name}" if expr.relation.name.to_s != table.name.to_s
|
53
|
+
visit name, Ansr::Arel::Visitors::Filter.new(attribute) if object.select
|
54
|
+
else
|
55
|
+
raise "Unexpected filter expression type #{object.expr.class}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_Ansr_Arel_Nodes_Facet(object, attribute)
|
60
|
+
expr = object.expr
|
61
|
+
case expr
|
62
|
+
when ::Arel::SqlLiteral
|
63
|
+
visit expr, Ansr::Arel::Visitors::Facet.new(attribute)
|
64
|
+
when ::Arel::Attributes::Attribute
|
65
|
+
name = object.expr.name
|
66
|
+
name = "#{expr.relation.name}.#{name}" if expr.relation.name.to_s != table.name.to_s
|
67
|
+
visit name, Ansr::Arel::Visitors::Facet.new(attribute) if object.select
|
68
|
+
else
|
69
|
+
raise "Unexpected filter expression type #{object.expr.class}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def projections
|
74
|
+
query_opts[:fields] || []
|
75
|
+
end
|
76
|
+
|
77
|
+
def filter_projections
|
78
|
+
query_opts[:facets] || []
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def field(field_name)
|
83
|
+
return unless field_name
|
84
|
+
old = query_opts[:fields] ? Array(query_opts[:fields]) : []
|
85
|
+
field_names = (old + Array(field_name)).uniq
|
86
|
+
if field_names[0]
|
87
|
+
query_opts[:fields] = field_names[1] ? field_names : field_names[0]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def filter_field(field_name)
|
92
|
+
return unless field_name
|
93
|
+
field_name = Array(field_name)
|
94
|
+
field_name.each {|fn| raise "#{fn} is not a facetable field" unless table.facets.include? fn.to_sym}
|
95
|
+
old = query_opts[:facets] ? Array(query_opts[:facets]) : []
|
96
|
+
field_names = (old + Array(field_name)).uniq
|
97
|
+
if field_names[0]
|
98
|
+
query_opts[:facets] = field_names[1] ? field_names : field_names[0]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_where_clause(attr_node, val)
|
103
|
+
field_key = field_key_from_node(attr_node)
|
104
|
+
if query_opts[field_key]
|
105
|
+
query_opts[field_key] = Array(query_opts[field_key]) << val
|
106
|
+
else
|
107
|
+
query_opts[field_key] = val
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# the DPLA API makes no distinction between filter and normal queries
|
112
|
+
def visit_Arel_Nodes_Equality(object, attribute)
|
113
|
+
add_where_clause(object.left, object.right)
|
114
|
+
end
|
115
|
+
|
116
|
+
def visit_Arel_Nodes_NotEqual(object, attribute)
|
117
|
+
add_where_clause(object.left, "NOT " + object.right)
|
118
|
+
end
|
119
|
+
def visit_Arel_Nodes_Or(object, attribute)
|
120
|
+
add_where_clause(object.left, "OR " + object.right)
|
121
|
+
end
|
122
|
+
|
123
|
+
def visit_Arel_Nodes_Grouping(object, attribute)
|
124
|
+
visit object.expr, attribute
|
125
|
+
end
|
126
|
+
|
127
|
+
def visit_Arel_Nodes_Ordering(object, attribute)
|
128
|
+
if query_opts[:sort_by]
|
129
|
+
query_opts[:sort_by] = Array[query_opts[:sort_by]] << object.expr.name
|
130
|
+
else
|
131
|
+
query_opts[:sort_by] = object.expr.name
|
132
|
+
end
|
133
|
+
direction = :asc if (::Arel::Nodes::Ascending === object and direction)
|
134
|
+
direction = :desc if (::Arel::Nodes::Descending === object)
|
135
|
+
query_opts[:sort_order] = direction if direction
|
136
|
+
end
|
137
|
+
|
138
|
+
def order(*arel_nodes)
|
139
|
+
direction = nil
|
140
|
+
nodes = []
|
141
|
+
arel_nodes.inject(nodes) do |c, n|
|
142
|
+
if ::Arel::Nodes::Ordering === n
|
143
|
+
c << n
|
144
|
+
elsif n.is_a? String
|
145
|
+
_ns = n.split(',')
|
146
|
+
_ns.each do |_n|
|
147
|
+
_p = _n.split(/\s+/)
|
148
|
+
if (_p[1])
|
149
|
+
_p[1] = _p[1].downcase.to_sym
|
150
|
+
else
|
151
|
+
_p[1] = :asc
|
152
|
+
end
|
153
|
+
c << table[_p[0].to_sym].send(_p[1])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
c
|
157
|
+
end
|
158
|
+
nodes.each do |node|
|
159
|
+
if ::Arel::Nodes::Ordering === node
|
160
|
+
if query_opts[:sort_by]
|
161
|
+
query_opts[:sort_by] = Array[query_opts[:sort_by]] << node.expr.name
|
162
|
+
else
|
163
|
+
query_opts[:sort_by] = node.expr.name
|
164
|
+
end
|
165
|
+
direction = :asc if (::Arel::Nodes::Ascending === node and direction)
|
166
|
+
direction = :desc if (::Arel::Nodes::Descending === node)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
query_opts[:sort_order] = direction if direction
|
170
|
+
end
|
171
|
+
|
172
|
+
def visit_Arel_Nodes_Limit(object, attribute)
|
173
|
+
value = object.expr
|
174
|
+
if value and (value = value.to_i)
|
175
|
+
raise "Page size cannot be > 500 (#{value}" if value > 500
|
176
|
+
query_opts[:page_size] = value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def visit_Arel_Nodes_Offset(object, attribute)
|
181
|
+
value = object.expr
|
182
|
+
if value
|
183
|
+
query_opts[:page] = (value.to_i / (query_opts[:page_size] || Ansr::Relation::DEFAULT_PAGE_SIZE)) + 1
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ansr::Dpla
|
2
|
+
module ConnectionAdapters
|
3
|
+
class NoSqlAdapter < Ansr::ConnectionAdapters::NoSqlAdapter
|
4
|
+
|
5
|
+
def self.connection_for(klass)
|
6
|
+
klass.api
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(klass, logger = nil, pool = nil) #:nodoc:
|
10
|
+
super(klass, klass.api, logger, pool)
|
11
|
+
@visitor = Ansr::Dpla::Arel::Visitors::ToNoSql.new(@table)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_sql(*args)
|
15
|
+
to_nosql(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(query, name='ANSR-DPLA')
|
19
|
+
method = query.path
|
20
|
+
query = query.to_h if Ansr::Dpla::Request === query
|
21
|
+
query = query.dup
|
22
|
+
aliases = query.delete(:aliases)
|
23
|
+
json = @connection.send(method, query)
|
24
|
+
json = json.length > 0 ? JSON.load(json) : {'docs' => [], 'facets' => []}
|
25
|
+
if json['docs'] and aliases
|
26
|
+
json['docs'].each do |doc|
|
27
|
+
aliases.each do |k,v|
|
28
|
+
if doc[k]
|
29
|
+
old = doc.delete(k)
|
30
|
+
if old and doc[v]
|
31
|
+
doc[v] = Array(doc[v]) if doc[v]
|
32
|
+
Array(old).each {|ov| doc[v] << ov}
|
33
|
+
else
|
34
|
+
doc[v] = old
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
json
|
41
|
+
end
|
42
|
+
|
43
|
+
def table_exists?(table_name)
|
44
|
+
['Collection', 'Item'].include? table_name
|
45
|
+
end
|
46
|
+
|
47
|
+
def sanitize_limit(limit_value)
|
48
|
+
if (0..500) === limit_value.to_s.to_i
|
49
|
+
limit_value
|
50
|
+
else
|
51
|
+
Ansr::Relation::DEFAULT_PAGE_SIZE
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|