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.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +127 -0
- data/ansr.gemspec +23 -0
- data/ansr_dpla/README.md +59 -0
- data/ansr_dpla/ansr_dpla.gemspec +27 -0
- data/ansr_dpla/app/models/collection.rb +2 -0
- data/ansr_dpla/app/models/item.rb +2 -0
- data/ansr_dpla/fixtures/collection.json +1 -0
- data/ansr_dpla/fixtures/collection.jsonld +14 -0
- data/ansr_dpla/fixtures/collections.json +1 -0
- data/ansr_dpla/fixtures/collections.jsonld +65 -0
- data/ansr_dpla/fixtures/dpla.yml +2 -0
- data/ansr_dpla/fixtures/empty.jsonld +1 -0
- data/ansr_dpla/fixtures/item.json +1 -0
- data/ansr_dpla/fixtures/item.jsonld +272 -0
- data/ansr_dpla/fixtures/kittens.json +1 -0
- data/ansr_dpla/fixtures/kittens.jsonld +2477 -0
- data/ansr_dpla/fixtures/kittens_faceted.json +1 -0
- data/ansr_dpla/fixtures/kittens_faceted.jsonld +2693 -0
- data/ansr_dpla/lib/ansr_dpla.rb +6 -0
- data/ansr_dpla/lib/ansr_dpla/api.rb +78 -0
- data/ansr_dpla/lib/ansr_dpla/arel.rb +8 -0
- data/ansr_dpla/lib/ansr_dpla/arel/big_table.rb +104 -0
- data/ansr_dpla/lib/ansr_dpla/arel/connection.rb +81 -0
- data/ansr_dpla/lib/ansr_dpla/arel/query_builder.rb +131 -0
- data/ansr_dpla/lib/ansr_dpla/model.rb +7 -0
- data/ansr_dpla/lib/ansr_dpla/model/base.rb +17 -0
- data/ansr_dpla/lib/ansr_dpla/model/pseudo_associate.rb +14 -0
- data/ansr_dpla/lib/ansr_dpla/model/querying.rb +38 -0
- data/ansr_dpla/spec/adpla_test_api.rb +9 -0
- data/ansr_dpla/spec/lib/api_spec.rb +110 -0
- data/ansr_dpla/spec/lib/item_spec.rb +57 -0
- data/ansr_dpla/spec/lib/relation/facet_spec.rb +74 -0
- data/ansr_dpla/spec/lib/relation/select_spec.rb +52 -0
- data/ansr_dpla/spec/lib/relation/where_spec.rb +74 -0
- data/ansr_dpla/spec/lib/relation_spec.rb +305 -0
- data/ansr_dpla/spec/spec_helper.rb +36 -0
- data/ansr_dpla/test/debug.rb +14 -0
- data/ansr_dpla/test/system.rb +50 -0
- data/lib/ansr.rb +16 -0
- data/lib/ansr/.DS_Store +0 -0
- data/lib/ansr/arel.rb +5 -0
- data/lib/ansr/arel/big_table.rb +24 -0
- data/lib/ansr/base.rb +29 -0
- data/lib/ansr/configurable.rb +20 -0
- data/lib/ansr/model.rb +159 -0
- data/lib/ansr/model/connection.rb +103 -0
- data/lib/ansr/model/connection_handler.rb +20 -0
- data/lib/ansr/relation.rb +121 -0
- data/lib/ansr/relation/arel_methods.rb +14 -0
- data/lib/ansr/relation/query_methods.rb +156 -0
- data/lib/ansr/sanitization.rb +36 -0
- data/lib/ansr/version.rb +6 -0
- metadata +196 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'rspec/autorun'
|
7
|
+
require 'loggable'
|
8
|
+
require 'ansr'
|
9
|
+
require 'ansr_dpla'
|
10
|
+
require 'adpla_test_api'
|
11
|
+
require 'blacklight'
|
12
|
+
require 'item'
|
13
|
+
require 'collection'
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def fixture_path(path)
|
20
|
+
File.join(File.dirname(__FILE__), '..', 'fixtures', path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fixture path, &block
|
24
|
+
if block_given?
|
25
|
+
open(fixture_path(path)) &block
|
26
|
+
else
|
27
|
+
open(fixture_path(path))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_fixture(path)
|
32
|
+
_f = fixture(path)
|
33
|
+
_f.read
|
34
|
+
ensure
|
35
|
+
_f and _f.close
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
|
2
|
+
require 'rails'
|
3
|
+
require 'adpla'
|
4
|
+
require 'item'
|
5
|
+
class Logger
|
6
|
+
def info(msg)
|
7
|
+
puts msg
|
8
|
+
end
|
9
|
+
alias :warn :info
|
10
|
+
alias :error :info
|
11
|
+
alias :debug :info
|
12
|
+
end
|
13
|
+
|
14
|
+
puts Item.table.inspect
|
@@ -0,0 +1,50 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app/models'))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
require 'ansr_dpla'
|
5
|
+
require 'item'
|
6
|
+
|
7
|
+
# you config the model with a hash including an API key for dp.la/v2, or the path to a YAML file
|
8
|
+
Item.config('config/dpla.yml')
|
9
|
+
|
10
|
+
# then you can find single items with known IDs
|
11
|
+
puts Item.find("7eb617e559007e1ad6d95bd30a30b16b")
|
12
|
+
|
13
|
+
|
14
|
+
# or you can search with ActiveRecord::Relations
|
15
|
+
opts = {q: 'kittens', facets: 'sourceResource.contributor'}
|
16
|
+
rel = Item.where(opts)
|
17
|
+
# this means the results are lazy-loaded, #load or #to_a will load them
|
18
|
+
rel.to_a
|
19
|
+
# you can also assemble the queries piecemeal with the Relation's decorator pattern
|
20
|
+
# the where decorator adds query fields
|
21
|
+
# the select decorator adds response fields
|
22
|
+
# the limit decorator adds a page size limit
|
23
|
+
# the offset decorator adds a non-zero starting point in the response set
|
24
|
+
# the filter decorator adds filter/facet fields and optionally values to query them on
|
25
|
+
rel = Item.where(q: 'kittens').limit(2).filter('sourceResource.contributor').select('sourceResource.title')
|
26
|
+
rel.to_a.each do |item|
|
27
|
+
puts "#{item["id"]} \"#{item['sourceResource.title']}\""
|
28
|
+
end
|
29
|
+
# the filter values for the query are available on the relation after it is loaded
|
30
|
+
rel.filters.each do |k,f|
|
31
|
+
puts "#{k} values"
|
32
|
+
f.items.each do |item|
|
33
|
+
puts "filter: \"#{item.value}\" : #{item.hits}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# the loaded Relation has attributes describing the response set
|
37
|
+
rel.count # the size of the response
|
38
|
+
# the where decorator can be negated
|
39
|
+
rel = rel.where.not(q: 'cats')
|
40
|
+
|
41
|
+
rel.to_a.each do |item|
|
42
|
+
puts "#{item["id"]} \"#{item['sourceResource.title']}\" \"#{item['originalRecord']}\""
|
43
|
+
end
|
44
|
+
|
45
|
+
rel.filters.each do |k,f|
|
46
|
+
puts "#{k} values"
|
47
|
+
f.items.each do |item|
|
48
|
+
puts " \"#{item.value}\" : #{item.hits}"
|
49
|
+
end
|
50
|
+
end
|
data/lib/ansr.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
module Ansr
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
eager_autoload do
|
5
|
+
autoload :Configurable
|
6
|
+
autoload :Arel
|
7
|
+
autoload :Base
|
8
|
+
autoload :Model
|
9
|
+
autoload :Sanitization
|
10
|
+
autoload :Relation
|
11
|
+
autoload_under 'relation' do
|
12
|
+
autoload :ArelMethods
|
13
|
+
autoload :QueryMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/ansr/.DS_Store
ADDED
Binary file
|
data/lib/ansr/arel.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ansr
|
2
|
+
module Arel
|
3
|
+
class BigTable < ::Arel::Table
|
4
|
+
attr_reader :fields, :facets, :sorts
|
5
|
+
|
6
|
+
include Ansr::Configurable
|
7
|
+
|
8
|
+
attr_reader :klass
|
9
|
+
alias :model :klass
|
10
|
+
|
11
|
+
def initialize(klass, engine=nil)
|
12
|
+
super(klass.name, engine.nil? ? klass.engine : engine)
|
13
|
+
@klass = klass.model
|
14
|
+
@fields = []
|
15
|
+
@facets = []
|
16
|
+
@sorts = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def view?
|
20
|
+
Ansr::Model::ViewProxy === model()
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/ansr/base.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ansr
|
2
|
+
class Base < ActiveRecord::Base
|
3
|
+
extend Ansr::Model::Methods
|
4
|
+
extend Ansr::Configurable
|
5
|
+
extend Ansr::QueryMethods
|
6
|
+
extend Ansr::ArelMethods
|
7
|
+
include Ansr::Sanitization
|
8
|
+
|
9
|
+
self.abstract_class = true
|
10
|
+
|
11
|
+
def initialize doc={}, options={}
|
12
|
+
super(filter_source_hash(doc), options)
|
13
|
+
@source_doc = doc
|
14
|
+
end
|
15
|
+
|
16
|
+
def filter_source_hash(doc)
|
17
|
+
fields = self.class.model().table().fields()
|
18
|
+
filtered = doc.select do |k,v|
|
19
|
+
fields.include? k.to_sym
|
20
|
+
end
|
21
|
+
filtered.with_indifferent_access
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](key)
|
25
|
+
@source_doc[key]
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Ansr
|
2
|
+
module Configurable
|
3
|
+
def config(yaml=nil)
|
4
|
+
yaml ? @config ||= begin
|
5
|
+
y = begin
|
6
|
+
case yaml
|
7
|
+
when String
|
8
|
+
File.open(yaml) {|blob| YAML.load(blob)}
|
9
|
+
else
|
10
|
+
yaml
|
11
|
+
end
|
12
|
+
end
|
13
|
+
y
|
14
|
+
end : @config
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :configure, :config
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/ansr/model.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
module Ansr
|
2
|
+
module Model
|
3
|
+
module Methods
|
4
|
+
def spawn
|
5
|
+
s = Ansr::Relation.new(model(), table())
|
6
|
+
s.references!(references())
|
7
|
+
end
|
8
|
+
|
9
|
+
def inherited(subclass)
|
10
|
+
super
|
11
|
+
# a hack for sanitize sql overrides to work, and some others where @klass used in place of klass()
|
12
|
+
subclass.instance_variable_set("@klass", subclass)
|
13
|
+
# a hack for the intermediate abstract model classes to work with table_name
|
14
|
+
subclass.instance_variable_set("@table_name", subclass.name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def model
|
18
|
+
m = begin
|
19
|
+
instance_variable_get "@klass"
|
20
|
+
end
|
21
|
+
raise "#{name()}.model() -> nil" unless m
|
22
|
+
m
|
23
|
+
end
|
24
|
+
|
25
|
+
def references
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
def table
|
30
|
+
raise 'Implementing classes must provide a BigTable reader'
|
31
|
+
end
|
32
|
+
|
33
|
+
def table=(table)
|
34
|
+
raise 'Implementing classes must provide a BigTable writer'
|
35
|
+
end
|
36
|
+
|
37
|
+
def engine
|
38
|
+
model()
|
39
|
+
end
|
40
|
+
|
41
|
+
def model
|
42
|
+
@klass
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_default_scope
|
46
|
+
Ansr::Relation.new(model(), table())
|
47
|
+
end
|
48
|
+
|
49
|
+
def view(*wheres)
|
50
|
+
return ViewProxy.new(model(), *wheres)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ViewProxy
|
55
|
+
attr_accessor :model
|
56
|
+
def initialize(model, *wheres)
|
57
|
+
if ViewProxy === model
|
58
|
+
@model = model.model
|
59
|
+
self.constraints = Array(model.constraints)
|
60
|
+
self.projections = model.projections.dup
|
61
|
+
else
|
62
|
+
@model = model
|
63
|
+
end
|
64
|
+
if wheres[0]
|
65
|
+
self.constraints = self.constraints + wheres
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def view_arel(engine, table)
|
70
|
+
arel = ::Arel::SelectManager.new(engine, table)
|
71
|
+
constraints.each {|c| arel.where(c) }
|
72
|
+
arel.project projections()
|
73
|
+
arel
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_by_nosql(arel, bind_values)
|
77
|
+
filter_context = nil
|
78
|
+
if self.view?
|
79
|
+
filter_context = view_arel(arel.engine, arel.source.left)
|
80
|
+
arel = arel.intersect filter_context
|
81
|
+
end
|
82
|
+
model.find_by_nosql(arel, bind_values)
|
83
|
+
end
|
84
|
+
|
85
|
+
def expand_hash_conditions_for_aggregates(attrs)
|
86
|
+
# this is a protected method in the AR sanitization module
|
87
|
+
model.send(:expand_hash_conditions_for_aggregates, attrs)
|
88
|
+
end
|
89
|
+
|
90
|
+
def constraints
|
91
|
+
@constraints ||= []
|
92
|
+
end
|
93
|
+
|
94
|
+
def constraints=(values)
|
95
|
+
@constraints = Array === (values) ? values : Array(values)
|
96
|
+
end
|
97
|
+
|
98
|
+
def projections
|
99
|
+
@projections ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
def projections=(values)
|
103
|
+
@projections = Array === (values) ? values : Array(values)
|
104
|
+
end
|
105
|
+
|
106
|
+
def view?
|
107
|
+
(@constraints and @constraints.length > 0) or (@projections and @projections.length > 0)
|
108
|
+
end
|
109
|
+
|
110
|
+
def view(*wheres)
|
111
|
+
ViewProxy.new(self, wheres)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.===(other)
|
115
|
+
other.is_a? ViewProxy
|
116
|
+
end
|
117
|
+
|
118
|
+
def connection_handler=(handler)
|
119
|
+
@connection_handler = handler
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_default_scope
|
123
|
+
model().all
|
124
|
+
end
|
125
|
+
|
126
|
+
# model delegations
|
127
|
+
def connection
|
128
|
+
model().connection
|
129
|
+
end
|
130
|
+
|
131
|
+
def name
|
132
|
+
model().name
|
133
|
+
end
|
134
|
+
|
135
|
+
def table
|
136
|
+
model().table
|
137
|
+
end
|
138
|
+
|
139
|
+
def arel_table
|
140
|
+
model().arel_table
|
141
|
+
end
|
142
|
+
|
143
|
+
def current_scope=(scope)
|
144
|
+
model().current_scope=(scope)
|
145
|
+
end
|
146
|
+
|
147
|
+
def current_scope
|
148
|
+
model().current_scope
|
149
|
+
end
|
150
|
+
|
151
|
+
def method_missing(method, *args, &block)
|
152
|
+
model().send(method, *args, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
require 'ansr/model/connection'
|
157
|
+
require 'ansr/model/connection_handler'
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Ansr
|
2
|
+
module Model
|
3
|
+
class Connection
|
4
|
+
def initialize(klass)
|
5
|
+
@table = klass.table
|
6
|
+
end
|
7
|
+
|
8
|
+
def primary_key(table_name)
|
9
|
+
'id'
|
10
|
+
end
|
11
|
+
|
12
|
+
def limit(arel_node)
|
13
|
+
case arel_node
|
14
|
+
when ::Arel::SelectManager
|
15
|
+
arel_node.limit
|
16
|
+
when ::Arel::Nodes::SelectStatement
|
17
|
+
arel_node.limit && arel_node.limit.expr
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def offset(arel_node)
|
24
|
+
case arel_node
|
25
|
+
when ::Arel::SelectManager
|
26
|
+
arel_node.offset
|
27
|
+
when ::Arel::Nodes::SelectStatement
|
28
|
+
arel_node.offset && arel_node.offset.expr
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def constraints(arel_node)
|
35
|
+
case arel_node
|
36
|
+
when ::Arel::SelectManager
|
37
|
+
arel_node.constraints
|
38
|
+
when ::Arel::Nodes::SelectStatement
|
39
|
+
arel_node.cores.last.wheres
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def projections(arel_node)
|
46
|
+
case arel_node
|
47
|
+
when ::Arel::SelectManager
|
48
|
+
arel_node.projections
|
49
|
+
when ::Arel::Nodes::SelectStatement
|
50
|
+
arel_node.cores.last.projections
|
51
|
+
else
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def orders(arel_node)
|
57
|
+
case arel_node
|
58
|
+
when ::Arel::SelectManager
|
59
|
+
arel_node.orders
|
60
|
+
when ::Arel::Nodes::SelectStatement
|
61
|
+
arel_node.orders
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def schema_cache
|
68
|
+
ActiveRecord::ConnectionAdapters::SchemaCache.new(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_exists?(table_name)
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
# this is called by the BigTable impl
|
76
|
+
def columns(table_name, *rest)
|
77
|
+
@table.fields.map {|s| ::ActiveRecord::ConnectionAdapters::Column.new(s.to_s, nil, nil)}
|
78
|
+
end
|
79
|
+
|
80
|
+
def sanitize_limit(limit_value)
|
81
|
+
if limit_value.to_s.to_i >= 0
|
82
|
+
limit_value
|
83
|
+
else
|
84
|
+
Ansr::Relation::DEFAULT_PAGE_SIZE
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def sanitize_filter_name(filter_value)
|
89
|
+
if filter_value.is_a? Array
|
90
|
+
return filter_value.collect {|x| sanitize_filter_name(x)}.compact
|
91
|
+
else
|
92
|
+
if @table.facets.include? filter_value.to_sym
|
93
|
+
return filter_value
|
94
|
+
else
|
95
|
+
raise "#{filter_value} is not a facetable field"
|
96
|
+
#Rails.logger.warn "Ignoring #{filter_value} (not a filterable field)" if Rails.logger
|
97
|
+
#return nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|