ansr_dpla 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +137 -0
  4. data/README.md +59 -0
  5. data/ansr_dpla.gemspec +27 -0
  6. data/app/models/collection.rb +2 -0
  7. data/app/models/item.rb +2 -0
  8. data/fixtures/collection.json +1 -0
  9. data/fixtures/collection.jsonld +14 -0
  10. data/fixtures/collections.json +1 -0
  11. data/fixtures/collections.jsonld +65 -0
  12. data/fixtures/dpla.yml +2 -0
  13. data/fixtures/empty.jsonld +1 -0
  14. data/fixtures/item.json +1 -0
  15. data/fixtures/item.jsonld +272 -0
  16. data/fixtures/kittens.json +1 -0
  17. data/fixtures/kittens.jsonld +2477 -0
  18. data/fixtures/kittens_faceted.json +1 -0
  19. data/fixtures/kittens_faceted.jsonld +2693 -0
  20. data/lib/ansr_dpla.rb +9 -0
  21. data/lib/ansr_dpla/api.rb +78 -0
  22. data/lib/ansr_dpla/arel.rb +7 -0
  23. data/lib/ansr_dpla/arel/big_table.rb +108 -0
  24. data/lib/ansr_dpla/arel/visitors.rb +6 -0
  25. data/lib/ansr_dpla/arel/visitors/query_builder.rb +188 -0
  26. data/lib/ansr_dpla/arel/visitors/to_no_sql.rb +9 -0
  27. data/lib/ansr_dpla/connection_adapters/no_sql_adapter.rb +58 -0
  28. data/lib/ansr_dpla/model.rb +7 -0
  29. data/lib/ansr_dpla/model/base.rb +24 -0
  30. data/lib/ansr_dpla/model/pseudo_associate.rb +14 -0
  31. data/lib/ansr_dpla/model/querying.rb +34 -0
  32. data/lib/ansr_dpla/relation.rb +60 -0
  33. data/lib/ansr_dpla/request.rb +5 -0
  34. data/spec/adpla_test_api.rb +9 -0
  35. data/spec/lib/api_spec.rb +113 -0
  36. data/spec/lib/item_spec.rb +57 -0
  37. data/spec/lib/relation/facet_spec.rb +82 -0
  38. data/spec/lib/relation/select_spec.rb +54 -0
  39. data/spec/lib/relation/where_spec.rb +74 -0
  40. data/spec/lib/relation_spec.rb +307 -0
  41. data/spec/spec_helper.rb +36 -0
  42. data/test/debug.rb +14 -0
  43. data/test/system.rb +52 -0
  44. metadata +236 -0
@@ -0,0 +1,9 @@
1
+ require 'ansr'
2
+ module Ansr::Dpla
3
+ require 'ansr_dpla/api'
4
+ require 'ansr_dpla/request'
5
+ require 'ansr_dpla/connection_adapters/no_sql_adapter'
6
+ require 'ansr_dpla/arel'
7
+ require 'ansr_dpla/relation'
8
+ require 'ansr_dpla/model'
9
+ end
@@ -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,7 @@
1
+ require 'active_record'
2
+ module Ansr::Dpla
3
+ module Arel
4
+ require 'ansr_dpla/arel/big_table'
5
+ require 'ansr_dpla/arel/visitors'
6
+ end
7
+ 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,6 @@
1
+ module Ansr::Dpla::Arel
2
+ module Visitors
3
+ require 'ansr_dpla/arel/visitors/query_builder'
4
+ require 'ansr_dpla/arel/visitors/to_no_sql'
5
+ end
6
+ 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,9 @@
1
+ module Ansr::Dpla::Arel::Visitors
2
+ class ToNoSql < Ansr::Arel::Visitors::ToNoSql
3
+
4
+ def query_builder(opts = nil)
5
+ Ansr::Dpla::Arel::Visitors::QueryBuilder.new(table, opts)
6
+ end
7
+
8
+ end
9
+ 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