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