ansr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +127 -0
- data/ansr.gemspec +23 -0
- data/ansr_dpla/README.md +59 -0
- data/ansr_dpla/ansr_dpla.gemspec +27 -0
- data/ansr_dpla/app/models/collection.rb +2 -0
- data/ansr_dpla/app/models/item.rb +2 -0
- data/ansr_dpla/fixtures/collection.json +1 -0
- data/ansr_dpla/fixtures/collection.jsonld +14 -0
- data/ansr_dpla/fixtures/collections.json +1 -0
- data/ansr_dpla/fixtures/collections.jsonld +65 -0
- data/ansr_dpla/fixtures/dpla.yml +2 -0
- data/ansr_dpla/fixtures/empty.jsonld +1 -0
- data/ansr_dpla/fixtures/item.json +1 -0
- data/ansr_dpla/fixtures/item.jsonld +272 -0
- data/ansr_dpla/fixtures/kittens.json +1 -0
- data/ansr_dpla/fixtures/kittens.jsonld +2477 -0
- data/ansr_dpla/fixtures/kittens_faceted.json +1 -0
- data/ansr_dpla/fixtures/kittens_faceted.jsonld +2693 -0
- data/ansr_dpla/lib/ansr_dpla.rb +6 -0
- data/ansr_dpla/lib/ansr_dpla/api.rb +78 -0
- data/ansr_dpla/lib/ansr_dpla/arel.rb +8 -0
- data/ansr_dpla/lib/ansr_dpla/arel/big_table.rb +104 -0
- data/ansr_dpla/lib/ansr_dpla/arel/connection.rb +81 -0
- data/ansr_dpla/lib/ansr_dpla/arel/query_builder.rb +131 -0
- data/ansr_dpla/lib/ansr_dpla/model.rb +7 -0
- data/ansr_dpla/lib/ansr_dpla/model/base.rb +17 -0
- data/ansr_dpla/lib/ansr_dpla/model/pseudo_associate.rb +14 -0
- data/ansr_dpla/lib/ansr_dpla/model/querying.rb +38 -0
- data/ansr_dpla/spec/adpla_test_api.rb +9 -0
- data/ansr_dpla/spec/lib/api_spec.rb +110 -0
- data/ansr_dpla/spec/lib/item_spec.rb +57 -0
- data/ansr_dpla/spec/lib/relation/facet_spec.rb +74 -0
- data/ansr_dpla/spec/lib/relation/select_spec.rb +52 -0
- data/ansr_dpla/spec/lib/relation/where_spec.rb +74 -0
- data/ansr_dpla/spec/lib/relation_spec.rb +305 -0
- data/ansr_dpla/spec/spec_helper.rb +36 -0
- data/ansr_dpla/test/debug.rb +14 -0
- data/ansr_dpla/test/system.rb +50 -0
- data/lib/ansr.rb +16 -0
- data/lib/ansr/.DS_Store +0 -0
- data/lib/ansr/arel.rb +5 -0
- data/lib/ansr/arel/big_table.rb +24 -0
- data/lib/ansr/base.rb +29 -0
- data/lib/ansr/configurable.rb +20 -0
- data/lib/ansr/model.rb +159 -0
- data/lib/ansr/model/connection.rb +103 -0
- data/lib/ansr/model/connection_handler.rb +20 -0
- data/lib/ansr/relation.rb +121 -0
- data/lib/ansr/relation/arel_methods.rb +14 -0
- data/lib/ansr/relation/query_methods.rb +156 -0
- data/lib/ansr/sanitization.rb +36 -0
- data/lib/ansr/version.rb +6 -0
- metadata +196 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
module Ansr::Dpla
|
3
|
+
class Api
|
4
|
+
include Ansr::Configurable
|
5
|
+
|
6
|
+
def config(yaml=nil)
|
7
|
+
super
|
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(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,104 @@
|
|
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
|
+
@fields += (opts[:fields] || FIELDS)
|
97
|
+
@facets += (opts[:facets] || FACETS)
|
98
|
+
@sorts += (opts[:sorts] || SORTS)
|
99
|
+
self.config(opts[:config]) if opts[:config]
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Ansr::Dpla
|
2
|
+
module Arel
|
3
|
+
class Connection < Ansr::Model::Connection
|
4
|
+
def initialize(klass)
|
5
|
+
super(klass)
|
6
|
+
@method = klass.name.downcase.pluralize.to_sym
|
7
|
+
@api = @table.engine.api
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_sql(*args)
|
11
|
+
to_nosql(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
# the object generated by this method will be passed to the self#execute
|
15
|
+
def to_nosql(select_manager, bind_values)
|
16
|
+
qb = Ansr::Dpla::Arel::QueryBuilder.new(@table)
|
17
|
+
if ::Arel::Nodes::Intersect === select_manager
|
18
|
+
filter_context = select_manager.right
|
19
|
+
select_manager = select_manager.left
|
20
|
+
constraints(filter_context).each {|c| qb.where(c)}
|
21
|
+
projections(filter_context).each {|c| qb.add_facet(c)}
|
22
|
+
end
|
23
|
+
constraints(select_manager).each {|c| qb.where(c)}
|
24
|
+
orders(select_manager).each {|c| qb.order(c)}
|
25
|
+
projections(select_manager).each {|c| qb.select(c)}
|
26
|
+
if (limit = limit(select_manager))
|
27
|
+
qb.take(limit)
|
28
|
+
end
|
29
|
+
if (offset = offset(select_manager))
|
30
|
+
qb.skip(offset)
|
31
|
+
end
|
32
|
+
qb.query_opts
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def to_aliases(select_manager, bind_values)
|
37
|
+
qb = Ansr::Dpla::Arel::QueryBuilder.new(@table)
|
38
|
+
if ::Arel::Nodes::Intersect === select_manager
|
39
|
+
select_manager = select_manager.left
|
40
|
+
end
|
41
|
+
projections(select_manager).each {|c| qb.select(c)}
|
42
|
+
qb.aliases
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute(query, aliases = {})
|
46
|
+
json = @api.send(@method, query)
|
47
|
+
json = json.length > 0 ? JSON.load(json) : {}
|
48
|
+
if json['docs'] and aliases
|
49
|
+
json['docs'].each do |doc|
|
50
|
+
aliases.each do |k,v|
|
51
|
+
if doc[k]
|
52
|
+
old = doc.delete(k)
|
53
|
+
if old and doc[v]
|
54
|
+
doc[v] = Array(doc[v]) if doc[v]
|
55
|
+
Array(old).each {|ov| doc[v] << ov}
|
56
|
+
else
|
57
|
+
doc[v] = old
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
json
|
64
|
+
end
|
65
|
+
|
66
|
+
def table_exists?(table_name)
|
67
|
+
['Collection', 'Item'].include? table_name
|
68
|
+
end
|
69
|
+
|
70
|
+
def sanitize_limit(limit_value)
|
71
|
+
if (0..500) === limit_value.to_s.to_i
|
72
|
+
limit_value
|
73
|
+
else
|
74
|
+
Ansr::Relation::DEFAULT_PAGE_SIZE
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Ansr::Dpla
|
2
|
+
module Arel
|
3
|
+
class QueryBuilder
|
4
|
+
attr_reader :query_opts, :aliases, :table
|
5
|
+
def initialize(big_table)
|
6
|
+
@query_opts = {}
|
7
|
+
@aliases = {}
|
8
|
+
@table = big_table
|
9
|
+
end
|
10
|
+
def fields=(value)
|
11
|
+
@query_opts[:fields] = value
|
12
|
+
end
|
13
|
+
def select(arel_nodes=nil)
|
14
|
+
arel_nodes = [arel_nodes].compact unless arel_nodes.respond_to? :each
|
15
|
+
arel_nodes.each.select {|an| ::Arel::SqlLiteral === an}.each do |n|
|
16
|
+
select_val = n.to_s.split(" AS ")
|
17
|
+
add_field(select_val[0])
|
18
|
+
aliases[select_val[0]] = select_val[1] if select_val[1]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def field_key_from_node(node)
|
23
|
+
table.model.field_name(node)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_field(field_name)
|
27
|
+
return unless field_name
|
28
|
+
query_opts[:fields] ||= ""
|
29
|
+
query_opts[:fields] << ',' << field_name
|
30
|
+
query_opts[:fields].sub!(/^,/,'')
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_facet(field_name)
|
34
|
+
return unless field_name
|
35
|
+
field_name = Array(field_name).uniq
|
36
|
+
if query_opts[:facets]
|
37
|
+
query_opts[:facets] = (Array(query_opts[:facets]) + field_name).uniq
|
38
|
+
else
|
39
|
+
query_opts[:facets] = field_name[1] ? field_name : field_name[0]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def where(arel_nodes)
|
44
|
+
arel_nodes = (arel_nodes.respond_to? :each) ? arel_nodes : Array(arel_nodes)
|
45
|
+
arel_nodes.each do |node|
|
46
|
+
case node
|
47
|
+
when ::Arel::Nodes::Equality
|
48
|
+
field_key = field_key_from_node(node.left)
|
49
|
+
if @query_opts[field_key]
|
50
|
+
@query_opts[field_key] = (Array(@query_opts[field_key]) << node.right)
|
51
|
+
else
|
52
|
+
@query_opts[field_key] = node.right
|
53
|
+
end
|
54
|
+
when ::Arel::Nodes::Grouping
|
55
|
+
n = node.expr
|
56
|
+
if ::Arel::Nodes::Binary === n
|
57
|
+
prefix = nil
|
58
|
+
prefix = "NOT" if (::Arel::Nodes::NotEqual === n)
|
59
|
+
prefix = "OR" if (::Arel::Nodes::Or === n)
|
60
|
+
if prefix
|
61
|
+
val = "#{prefix} #{n.right}"
|
62
|
+
else
|
63
|
+
val = n.right
|
64
|
+
end
|
65
|
+
field_key = field_key_from_node(n)
|
66
|
+
if @query_opts[field_key]
|
67
|
+
@query_opts[field_key] = Array(@query_opts[field_key]) << val
|
68
|
+
else
|
69
|
+
@query_opts[field_key] = val
|
70
|
+
end
|
71
|
+
end
|
72
|
+
when ::Arel::Attributes::Attribute # this is the field references
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def order(*arel_nodes)
|
78
|
+
direction = nil
|
79
|
+
nodes = []
|
80
|
+
arel_nodes.inject(nodes) do |c, n|
|
81
|
+
if ::Arel::Nodes::Ordering === n
|
82
|
+
c << n
|
83
|
+
elsif n.is_a? String
|
84
|
+
_ns = n.split(',')
|
85
|
+
_ns.each do |_n|
|
86
|
+
_p = _n.split(/\s+/)
|
87
|
+
if (_p[1])
|
88
|
+
_p[1] = _p[1].downcase.to_sym
|
89
|
+
else
|
90
|
+
_p[1] = :asc
|
91
|
+
end
|
92
|
+
c << table[_p[0].to_sym].send(_p[1])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
c
|
96
|
+
end
|
97
|
+
nodes.each do |node|
|
98
|
+
if ::Arel::Nodes::Ordering === node
|
99
|
+
if @query_opts[:sort_by]
|
100
|
+
@query_opts[:sort_by] = Array[@query_opts[:sort_by]] << node.expr.name
|
101
|
+
else
|
102
|
+
@query_opts[:sort_by] = node.expr.name
|
103
|
+
end
|
104
|
+
direction = :asc if (::Arel::Nodes::Ascending === node and direction)
|
105
|
+
direction = :desc if (::Arel::Nodes::Descending === node)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
@query_opts[:sort_order] = direction if direction
|
109
|
+
end
|
110
|
+
|
111
|
+
def take(value=nil)
|
112
|
+
if value and (value = value.to_i)
|
113
|
+
raise "Page size cannot be > 500 (#{value}" if value > 500
|
114
|
+
@query_opts[:page_size] = value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def skip(value=nil)
|
119
|
+
if value
|
120
|
+
@query_opts[:page] = (value.to_i / (@query_opts[:page_size] || Ansr::Relation::DEFAULT_PAGE_SIZE)) + 1
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def filters=(values)
|
125
|
+
unless values.empty?
|
126
|
+
@query_opts[:facets] = (values[1]) ? values : values[0]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'ansr'
|
2
|
+
module Ansr::Dpla
|
3
|
+
module Model
|
4
|
+
class Base < Ansr::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
include Querying
|
8
|
+
|
9
|
+
def assign_nested_parameter_attributes(pairs)
|
10
|
+
pairs.each do |k, v|
|
11
|
+
v = PseudoAssociate.new(v) if Hash === v
|
12
|
+
_assign_attribute(k, v)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# a class to pretend the unfindable "associations" are real models
|
2
|
+
module Ansr::Dpla
|
3
|
+
module Model
|
4
|
+
class PseudoAssociate
|
5
|
+
def initialize(doc = {})
|
6
|
+
@doc = doc.with_indifferent_access
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args)
|
10
|
+
@doc[name] or super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|