ansr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +127 -0
  4. data/ansr.gemspec +23 -0
  5. data/ansr_dpla/README.md +59 -0
  6. data/ansr_dpla/ansr_dpla.gemspec +27 -0
  7. data/ansr_dpla/app/models/collection.rb +2 -0
  8. data/ansr_dpla/app/models/item.rb +2 -0
  9. data/ansr_dpla/fixtures/collection.json +1 -0
  10. data/ansr_dpla/fixtures/collection.jsonld +14 -0
  11. data/ansr_dpla/fixtures/collections.json +1 -0
  12. data/ansr_dpla/fixtures/collections.jsonld +65 -0
  13. data/ansr_dpla/fixtures/dpla.yml +2 -0
  14. data/ansr_dpla/fixtures/empty.jsonld +1 -0
  15. data/ansr_dpla/fixtures/item.json +1 -0
  16. data/ansr_dpla/fixtures/item.jsonld +272 -0
  17. data/ansr_dpla/fixtures/kittens.json +1 -0
  18. data/ansr_dpla/fixtures/kittens.jsonld +2477 -0
  19. data/ansr_dpla/fixtures/kittens_faceted.json +1 -0
  20. data/ansr_dpla/fixtures/kittens_faceted.jsonld +2693 -0
  21. data/ansr_dpla/lib/ansr_dpla.rb +6 -0
  22. data/ansr_dpla/lib/ansr_dpla/api.rb +78 -0
  23. data/ansr_dpla/lib/ansr_dpla/arel.rb +8 -0
  24. data/ansr_dpla/lib/ansr_dpla/arel/big_table.rb +104 -0
  25. data/ansr_dpla/lib/ansr_dpla/arel/connection.rb +81 -0
  26. data/ansr_dpla/lib/ansr_dpla/arel/query_builder.rb +131 -0
  27. data/ansr_dpla/lib/ansr_dpla/model.rb +7 -0
  28. data/ansr_dpla/lib/ansr_dpla/model/base.rb +17 -0
  29. data/ansr_dpla/lib/ansr_dpla/model/pseudo_associate.rb +14 -0
  30. data/ansr_dpla/lib/ansr_dpla/model/querying.rb +38 -0
  31. data/ansr_dpla/spec/adpla_test_api.rb +9 -0
  32. data/ansr_dpla/spec/lib/api_spec.rb +110 -0
  33. data/ansr_dpla/spec/lib/item_spec.rb +57 -0
  34. data/ansr_dpla/spec/lib/relation/facet_spec.rb +74 -0
  35. data/ansr_dpla/spec/lib/relation/select_spec.rb +52 -0
  36. data/ansr_dpla/spec/lib/relation/where_spec.rb +74 -0
  37. data/ansr_dpla/spec/lib/relation_spec.rb +305 -0
  38. data/ansr_dpla/spec/spec_helper.rb +36 -0
  39. data/ansr_dpla/test/debug.rb +14 -0
  40. data/ansr_dpla/test/system.rb +50 -0
  41. data/lib/ansr.rb +16 -0
  42. data/lib/ansr/.DS_Store +0 -0
  43. data/lib/ansr/arel.rb +5 -0
  44. data/lib/ansr/arel/big_table.rb +24 -0
  45. data/lib/ansr/base.rb +29 -0
  46. data/lib/ansr/configurable.rb +20 -0
  47. data/lib/ansr/model.rb +159 -0
  48. data/lib/ansr/model/connection.rb +103 -0
  49. data/lib/ansr/model/connection_handler.rb +20 -0
  50. data/lib/ansr/relation.rb +121 -0
  51. data/lib/ansr/relation/arel_methods.rb +14 -0
  52. data/lib/ansr/relation/query_methods.rb +156 -0
  53. data/lib/ansr/sanitization.rb +36 -0
  54. data/lib/ansr/version.rb +6 -0
  55. metadata +196 -0
@@ -0,0 +1,6 @@
1
+ require 'ansr'
2
+ module Ansr::Dpla
3
+ require 'ansr_dpla/api'
4
+ require 'ansr_dpla/arel'
5
+ require 'ansr_dpla/model'
6
+ end
@@ -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,8 @@
1
+ require 'active_record'
2
+ module Ansr::Dpla
3
+ module Arel
4
+ require 'ansr_dpla/arel/big_table'
5
+ require 'ansr_dpla/arel/connection'
6
+ require 'ansr_dpla/arel/query_builder'
7
+ end
8
+ 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,7 @@
1
+ module Ansr::Dpla
2
+ module Model
3
+ autoload :PseudoAssociate, 'ansr_dpla/model/pseudo_associate'
4
+ require 'ansr_dpla/model/querying'
5
+ require 'ansr_dpla/model/base'
6
+ end
7
+ 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