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,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,24 @@
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 self.inherited(subclass)
10
+ super(subclass)
11
+ subclass.configure do |config|
12
+ config[:table_class] = Ansr::Dpla::Arel::BigTable
13
+ end
14
+ end
15
+
16
+ def assign_nested_parameter_attributes(pairs)
17
+ pairs.each do |k, v|
18
+ v = PseudoAssociate.new(v) if Hash === v
19
+ _assign_attribute(k, v)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ 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
@@ -0,0 +1,34 @@
1
+ require 'active_record'
2
+ module Ansr::Dpla
3
+ module Model
4
+ module Querying
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def build_default_scope
9
+ Ansr::Dpla::Relation.new(model(), table())
10
+ end
11
+
12
+ def api
13
+ @api ||= begin
14
+ a = (config[:api] || Ansr::Dpla::Api).new
15
+ a.config{|x| x.merge!(self.config)}
16
+ a
17
+ end
18
+ end
19
+
20
+ def api=(api)
21
+ @api = api
22
+ end
23
+
24
+ def connection_handler
25
+ @connection_handler ||= Ansr::Model::ConnectionHandler.new(Ansr::Dpla::ConnectionAdapters::NoSqlAdapter)
26
+ end
27
+
28
+ def references
29
+ ['provider', 'object']
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+ module Ansr::Dpla
3
+ class Relation < ::Ansr::Relation
4
+
5
+ def initialize(klass, table, values = {})
6
+ raise "Cannot search nil model" if klass.nil?
7
+ super(klass, table, values)
8
+ end
9
+
10
+ def facet_values=(values)
11
+ values.each {|value| raise "#{value.expr.name.to_sym} is not facetable" unless table.facets.include? value.expr.name.to_sym}
12
+ super
13
+ end
14
+
15
+ def empty?
16
+ count == 0
17
+ end
18
+
19
+ def many?
20
+ count > 1
21
+ end
22
+
23
+ def offset!(value)
24
+ page_size = self.limit_value || default_limit_value
25
+ if (value.to_i % page_size.to_i) != 0
26
+ raise "Bad offset #{value} for page size #{page_size}"
27
+ end
28
+ self.offset_value=value
29
+ self
30
+ end
31
+
32
+ def count
33
+ self.load
34
+ @response['count']
35
+ end
36
+
37
+ def filters_from(response)
38
+ f = {}
39
+ (response['facets'] || {}).inject(f) do |h,(k,v)|
40
+
41
+ if v['total'] != 0
42
+ items = v['terms'].collect do |term|
43
+ Ansr::Facets::FacetItem.new(:value => term['term'], :hits => term['count'])
44
+ end
45
+ options = {:sort => 'asc', :offset => 0}
46
+ h[k] = Ansr::Facets::FacetField.new k, items, options
47
+ end
48
+ h
49
+ end
50
+ f
51
+ end
52
+
53
+ def docs_from(response)
54
+ response['docs']
55
+ end
56
+
57
+ end
58
+
59
+
60
+ end
@@ -0,0 +1,5 @@
1
+ module Ansr::Dpla
2
+ class Request < ::Ansr::OpenStructWithHashAccess
3
+ attr_accessor :path
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Ansr::Dpla
2
+ class TestApi
3
+ include Ansr::Configurable
4
+ def items(opts={})
5
+ end
6
+ def collections(opts={})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ansr::Dpla::Api do
4
+
5
+ describe '#config' do
6
+ before do
7
+ @test = Ansr::Dpla::Api.new
8
+ end
9
+
10
+ it "should be configurable with a Hash" do
11
+ config_fixture = {:api_key => :foo, :url => :bar}
12
+ @test.config{|x| x.merge!(config_fixture)}
13
+ expect(@test.url).to eql(:bar)
14
+ end
15
+
16
+ it "should be configurable with a path to a yaml" do
17
+ open(fixture_path('dpla.yml')) do |blob|
18
+ config = YAML.load(blob)
19
+ @test.config{|x| x.merge!(config)}
20
+ end
21
+ expect(@test.url).to eql('http://fake.dp.la/v0/')
22
+ end
23
+
24
+ it "should raise an error of the config doesn't have required fields" do
25
+ expect { Ansr::Dpla::Api.new.config{|x| x.merge!(:url => :foo)}}.to raise_error
26
+ Ansr::Dpla::Api.new.config {|x| x.merge!({:url => :foo, :api_key => :foo})}
27
+ end
28
+
29
+ end
30
+
31
+ describe '#path_for' do
32
+ before do
33
+ @test = Ansr::Dpla::Api.new
34
+ @test.config{|x| x.merge!(:api_key => :testing)}
35
+ end
36
+
37
+
38
+ describe 'queries' do
39
+ it "should build paths for basic queries" do
40
+ fixture = {:foo => ['kittens', 'cats'], :bar => ['puppies', 'dogs']}
41
+ expected = 'tests?api_key=testing&foo=kittens+AND+cats&bar=puppies+AND+dogs'
42
+ expect(@test.path_for('tests', fixture)).to eql(expected)
43
+ expect(@test.items_path(fixture)).to eql(expected.sub(/^tests/,'items'))
44
+ expect(@test.collections_path(fixture)).to eql(expected.sub(/^tests/,'collections'))
45
+ expect(@test.item_path('foo')).to eql('items/foo?api_key=testing')
46
+ expect(@test.collection_path('foo')).to eql('collections/foo?api_key=testing')
47
+ end
48
+
49
+ it "should build paths for OR and NOT queries" do
50
+ fixture = {:foo => ['kittens', 'NOT cats']}
51
+ expected = 'tests?api_key=testing&foo=kittens+AND+NOT+cats'
52
+ expect(@test.path_for('tests', fixture)).to eql(expected)
53
+
54
+ fixture = {:foo => ['NOT kittens', 'cats']}
55
+ expected = 'tests?api_key=testing&foo=NOT+kittens+AND+cats'
56
+ expect(@test.path_for('tests', fixture)).to eql(expected)
57
+
58
+ fixture = {:foo => ['kittens', 'OR cats']}
59
+ expected = 'tests?api_key=testing&foo=kittens+OR+cats'
60
+ expect(@test.path_for('tests', fixture)).to eql(expected)
61
+
62
+ fixture = {:foo => ['OR kittens', 'cats']}
63
+ expected = 'tests?api_key=testing&foo=kittens+AND+cats'
64
+ expect(@test.path_for('tests', fixture)).to eql(expected)
65
+ end
66
+ end
67
+
68
+ it "should build paths for facets right" do
69
+ fixture = {:foo => 'kittens', :facets => :bar}
70
+ expected = 'tests?api_key=testing&foo=kittens&facets=bar'
71
+ expect(@test.path_for('tests', fixture)).to eql(expected)
72
+
73
+ fixture = {:foo => 'kittens', :facets => [:bar, :baz]}
74
+ expected = 'tests?api_key=testing&foo=kittens&facets=bar%2Cbaz'
75
+ expect(@test.path_for('tests', fixture)).to eql(expected)
76
+
77
+ fixture = {:foo => 'kittens', :facets => 'bar,baz'}
78
+ expected = 'tests?api_key=testing&foo=kittens&facets=bar%2Cbaz'
79
+ expect(@test.path_for('tests', fixture)).to eql(expected)
80
+ end
81
+
82
+ it "should build paths for sorts right" do
83
+ fixture = {:foo => 'kittens', :sort_by => [:bar, :baz]}
84
+ expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz'
85
+ expect(@test.path_for('tests', fixture)).to eql(expected)
86
+
87
+ fixture = {:foo => 'kittens', :sort_by => 'bar,baz'}
88
+ expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz'
89
+ expect(@test.path_for('tests', fixture)).to eql(expected)
90
+
91
+ fixture = {:foo => 'kittens', :sort_by => 'bar,baz', :sort_order => :desc}
92
+ expected = 'tests?api_key=testing&foo=kittens&sort_by=bar%2Cbaz&sort_order=desc'
93
+ expect(@test.path_for('tests', fixture)).to eql(expected)
94
+ end
95
+
96
+ it "should build paths for field selections right" do
97
+ fixture = {:foo => 'kittens', :fields => [:bar, :baz]}
98
+ expected = 'tests?api_key=testing&foo=kittens&fields=bar%2Cbaz'
99
+ expect(@test.path_for('tests', fixture)).to eql(expected)
100
+
101
+ fixture = {:foo => 'kittens', :fields => 'bar,baz'}
102
+ expected = 'tests?api_key=testing&foo=kittens&fields=bar%2Cbaz'
103
+ expect(@test.path_for('tests', fixture)).to eql(expected)
104
+ end
105
+
106
+ it "should build paths for limits and page sizes right" do
107
+ fixture = {:foo => 'kittens', :page_size=>25, :page=>4}
108
+ expected = 'tests?api_key=testing&foo=kittens&page_size=25&page=4'
109
+ expect(@test.path_for('tests', fixture)).to eql(expected)
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Item do
4
+ describe '.configure' do
5
+ it "should store the configured Api class" do
6
+ Item.configure {|x| x.merge!(:api=>Ansr::Dpla::TestApi, :api_key => :dummy)}
7
+ expect(Item.api).to be_a Ansr::Dpla::TestApi
8
+ end
9
+ end
10
+
11
+ describe '.find' do
12
+ it "should find an item given an id" do
13
+ mock_api = double('api')
14
+ Item.api = mock_api
15
+ mock_api.should_receive(:items).with(:id=>"123", :page_size=>1).and_return(read_fixture('item.jsonld'))
16
+ Item.find('123')
17
+ end
18
+ it "should raise an exception for a bad id" do
19
+ mock_api = double('api')
20
+ Item.api = mock_api
21
+ mock_api.should_receive(:items).with(:id=>"123", :page_size=>1).and_return(read_fixture('empty.jsonld'))
22
+ expect {Item.find('123')}.to raise_error
23
+ end
24
+ end
25
+
26
+ describe '.where' do
27
+ before do
28
+ Item.configure{|x| x.merge!({:api=>Ansr::Dpla::TestApi, :api_key => :dummy})}
29
+ end
30
+ it 'should return a Relation when there is query information' do
31
+ expect(Item.where({:q=>'kittens'})).to be_a Ansr::Relation
32
+ end
33
+ it 'should return itself when there is no query information' do
34
+ expect(Item.where({})).to be Item
35
+ end
36
+ end
37
+
38
+ describe 'accessor methods' do
39
+ before do
40
+ mock_api = double('api')
41
+ Item.api = mock_api
42
+ @hash = JSON.parse(read_fixture('item.jsonld'))['docs'][0]
43
+ @test = Item.new(@hash)
44
+ end
45
+
46
+ it 'should dispatch method names to the hash' do
47
+ @test.dataProvider.should == "Boston Public Library"
48
+ @test.sourceResource.identifier.should == [ "Local accession: 08_06_000884" ]
49
+ end
50
+
51
+ it 'should miss methods for undefined fields' do
52
+ expect {@test.foo}.to raise_error
53
+ end
54
+ end
55
+
56
+
57
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ansr::Dpla::Relation do
4
+ before do
5
+ @kittens = read_fixture('kittens.jsonld')
6
+ @faceted = read_fixture('kittens_faceted.jsonld')
7
+ @empty = read_fixture('empty.jsonld')
8
+ @mock_api = double('api')
9
+ Item.config{ |x| x[:api_key] = :foo}
10
+ Item.engine.api= @mock_api
11
+ end
12
+
13
+ subject { Ansr::Dpla::Relation.new(Item, Item.table) }
14
+ describe '#filter' do
15
+ describe "do a single field, single value filter" do
16
+ it "with facet" do
17
+ test = subject.filter(:object=>'kittens').facet(:object)
18
+ @mock_api.should_receive(:items).with(:object => 'kittens', :facets => :object).and_return('')
19
+ test.load
20
+ end
21
+ it "without facet" do
22
+ test = subject.filter(:object=>'kittens')
23
+ @mock_api.should_receive(:items).with(:object => 'kittens').and_return('')
24
+ test.load
25
+ end
26
+ end
27
+ it "do a single field, multiple value filter" do
28
+ test = subject.filter(:object=>['kittens', 'cats']).facet(:object)
29
+ @mock_api.should_receive(:items).with(:object => ['kittens','cats'], :facets => :object).and_return('')
30
+ test.load
31
+ end
32
+ it "do merge single field, multiple value filters" do
33
+ test = subject.filter(:"provider.name"=>'kittens').filter(:"provider.name"=>'cats').facet(:"provider.name")
34
+ @mock_api.should_receive(:items).with(hash_including(:"provider.name" => ['kittens','cats'], :facets => :"provider.name")).and_return('')
35
+ test.load
36
+ end
37
+ it "do a multiple field, single value filter" do
38
+ test = subject.filter(:object=>'kittens',:isShownAt=>'bears').facet([:object, :isShownAt])
39
+ @mock_api.should_receive(:items).with(hash_including(:object => 'kittens', :isShownAt=>'bears', :facets => [:object, :isShownAt])).and_return('')
40
+ test.load
41
+ end
42
+ it "should keep scope distinct from spawned Relations" do
43
+ test = subject.filter(:"provider.name"=>'kittens').facet(:"provider.name")
44
+ test.where(:q=>'cats')
45
+ @mock_api.should_receive(:items).with(:"provider.name" => 'kittens', :facets => :"provider.name").and_return('')
46
+ test.load
47
+ end
48
+ it "should raise an error if the requested field is not a facetable field" do
49
+ expect {subject.facet(:foo)}.to raise_error
50
+ end
51
+ it "should facet without a filter" do
52
+ test = subject.facet(:object)
53
+ @mock_api.should_receive(:items).with(:facets => :object).and_return('')
54
+ test.load
55
+ end
56
+ end
57
+
58
+ describe '#filters' do
59
+ it 'should return Blacklight types' do
60
+ # Blacklight::SolrResponse::Facets::FacetItem.new(:value => s, :hits => v)
61
+ # options = {:sort => 'asc', :offset => 0}
62
+ # Blacklight::SolrResponse::Facets::FacetField.new name, items, options
63
+ test = subject.where(:q=>'kittens')
64
+ @mock_api.should_receive(:items).with(:q => 'kittens').and_return(@faceted)
65
+ test.load
66
+ fkey = test.filters.keys.first
67
+ facet = test.filters[fkey]
68
+ expect(facet).to be_a(Ansr::Facets::FacetField)
69
+ facet.items
70
+ end
71
+ it 'should dispatch a query with no docs requested if not loaded' do
72
+ test = subject.where(:q=>'kittens')
73
+ @mock_api.should_receive(:items).with(:q => 'kittens', :page_size=>0).once.and_return(@faceted)
74
+ fkey = test.filters.keys.first
75
+ facet = test.filters[fkey]
76
+ expect(facet).to be_a(Ansr::Facets::FacetField)
77
+ expect(test.loaded?).to be_false
78
+ test.filters # make sure we memoized the facet values
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ansr::Relation do
4
+ before do
5
+ @kittens = read_fixture('kittens.jsonld')
6
+ @faceted = read_fixture('kittens_faceted.jsonld')
7
+ @empty = read_fixture('empty.jsonld')
8
+ @mock_api = double('api')
9
+ Item.config{ |x| x[:api_key] = :foo}
10
+ Item.engine.api= @mock_api
11
+ end
12
+
13
+ subject { Ansr::Dpla::Relation.new(Item, Item.table) }
14
+
15
+ describe '#select' do
16
+ describe 'with a block given' do
17
+ it "should build an array" do
18
+ test = subject.where(q:'kittens')
19
+ @mock_api.should_receive(:items).with(:q => 'kittens').and_return(@kittens)
20
+ actual = test.select {|d| true}
21
+ expect(actual).to be_a(Array)
22
+ expect(actual.length).to eql(test.limit_value)
23
+ actual = test.select {|d| false}
24
+ expect(actual).to be_a(Array)
25
+ expect(actual.length).to eql(0)
26
+ end
27
+ end
28
+ describe 'with a String or Symbol key given' do
29
+ it 'should change the requested document fields' do
30
+ test = subject.where(q:'kittens')
31
+ @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:name).and_return('')
32
+ test = test.select('name')
33
+ test.load
34
+ end
35
+ end
36
+ describe 'with a list of keys' do
37
+ it "should add all the requested document fields" do
38
+ test = subject.where(q:'kittens')
39
+ @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>[:name,:foo]).and_return('')
40
+ test = test.select(['name','foo'])
41
+ test.load
42
+ end
43
+ it "should add all the requested document fields and proxy them" do
44
+ test = subject.where(q:'kittens')
45
+ @mock_api.should_receive(:items).with(:q => 'kittens', :fields=>:object).and_return(@kittens)
46
+ test = test.select('object AS my_object')
47
+ test.load
48
+ expect(test.to_a.first['object']).to be_nil
49
+ expect(test.to_a.first['my_object']).to eql('http://ark.digitalcommonwealth.org/ark:/50959/6682xj30d/thumbnail')
50
+ end
51
+ end
52
+ end
53
+
54
+ end