ansr_dpla 0.0.4
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 +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
|