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