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