Skip to content

Commit 413e0c0

Browse files
authored
Merge pull request #78 from plural/printing-search
Printing search
2 parents afcdff7 + 45e313f commit 413e0c0

File tree

8 files changed

+849
-38
lines changed

8 files changed

+849
-38
lines changed

app/models/printing.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ class Printing < ApplicationRecord
88
has_one :side, :through => :card
99
has_many :illustrator_printings
1010
has_many :illustrators, :through => :illustrator_printings
11+
12+
has_many :unified_restrictions, primary_key: :card_id, foreign_key: :card_id
13+
has_many :card_pool_cards, primary_key: :card_id, foreign_key: :card_id
14+
has_many :card_pools, :through => :card_pool_cards
1115
end

app/resources/api/v3/public/printing_resource.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,17 @@ class Api::V3::Public::PrintingResource < JSONAPI::Resource
8383
filter :is_unique, apply: ->(records, value, _options){
8484
records.joins(:card).where('cards.is_unique= ?', value)
8585
}
86-
86+
filter :search, apply: ->(records, value, _options) {
87+
query_builder = PrintingSearchQueryBuilder.new(value[0])
88+
if query_builder.parse_error.nil?
89+
records.left_joins(query_builder.left_joins)
90+
.where(query_builder.where, *query_builder.where_values)
91+
else
92+
raise JSONAPI::Exceptions::BadRequest.new(
93+
'Invalid search query: [%s] / %s' % [value[0], query_builder.parse_error])
94+
end
95+
}
96+
8797
# Images will return a nested map for different types of images.
8898
# 'nrdb_classic' represents the JPEGs used for classic netrunnerdb.com.
8999
# We will likely add other formats like png and webp, as well as various sizes,

lib/card_search_parser.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ class CardSearchParser < Parslet::Parser
2121
str('advancement_cost') |
2222
str('agenda_points') |
2323
str('base_link') |
24-
str('card_cycle') |
2524
str('card_pool') |
26-
str('card_set') |
2725
str('card_subtype') |
2826
str('card_type') |
2927
str('cost') |
3028
str('eternal_points') |
3129
str('faction') |
32-
str('flavor_text') |
3330
str('format') |
3431
str('global_penalty') |
3532
str('illustrator') |
@@ -38,8 +35,6 @@ class CardSearchParser < Parslet::Parser
3835
str('is_restricted') |
3936
str('is_unique') |
4037
str('memory_usage') |
41-
str('quantity_in_card_set') |
42-
str('release_date') |
4338
str('restriction_id') |
4439
str('side') |
4540
str('strength') |

lib/card_search_query_builder.rb

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,6 @@ class CardSearchQueryBuilder
6060
}
6161
@@term_to_field_map = {
6262
# format should implicitly use the currently active card pool and restriction lists unless another is specified.
63-
# 'format' => '',
64-
# 'restriction' => '',
65-
66-
# printing? or minimum release date from printing for the card? Add release date to the card? 'r' => 'release_date',
67-
# printing 'a' => 'flavor',
68-
# printing 'c' => 'card_cycle_id',
69-
# printing 'card_cycle' => 'card_cycle_id'',
70-
# printing 'card_set' => 'card_set_id'',
71-
# printing 'e' => 'card_set_id',
72-
# printing 'i' => 'illustrator',
73-
# printing 'quantity_in_card_set' => ''',
74-
# printing 'release_date' => ''',
75-
# printing flavor 'flavor_text' => ''',
76-
# printing illustrator 'illustrator' => ''',
77-
# printing quantity 'y' => ''',
78-
7963
'_' => 'cards.stripped_title',
8064
'advancement_cost' => 'cards.advancement_requirement',
8165
'agenda_points' => 'cards.agenda_points',
@@ -88,6 +72,7 @@ class CardSearchQueryBuilder
8872
'eternal_points' => 'unified_restrictions.eternal_points',
8973
'f' => 'cards.faction_id',
9074
'faction' => 'cards.faction_id',
75+
'format' => 'unified_restrictions.format_id',
9176
'g' => 'cards.advancement_requirement',
9277
'global_penalty' => 'unified_restrictions.global_penalty',
9378
'h' => 'cards.trash_cost',
@@ -115,11 +100,23 @@ class CardSearchQueryBuilder
115100
'x' => 'cards.stripped_text',
116101
}
117102

103+
@@term_to_left_join_map = {
104+
'card_pool' => :card_pool_cards,
105+
'card_subtype' => :card_subtypes,
106+
'eternal_points' => :unified_restrictions,
107+
'global_penalty' => :unified_restrictions,
108+
'is_banned' => :unified_restrictions,
109+
'is_restricted' => :unified_restrictions,
110+
'restriction_id' => :unified_restrictions,
111+
's' => :card_subtypes,
112+
'universal_faction_cost' => :unified_restrictions,
113+
}
114+
118115
def initialize(query)
119116
@query = query
120117
@parse_error = nil
121118
@parse_tree = nil
122-
@left_joins = []
119+
@left_joins = Set.new
123120
@where = ''
124121
@where_values = []
125122
begin
@@ -152,9 +149,6 @@ def initialize(query)
152149
@parse_error = 'Invalid boolean operator "%s"' % match_type
153150
return
154151
end
155-
if ['is_banned', 'is_restricted'].include?(keyword)
156-
@left_joins << :unified_restrictions
157-
end
158152
constraints << '%s %s ?' % [@@term_to_field_map[keyword], operator]
159153
where << value
160154
elsif @@numeric_keywords.include?(keyword)
@@ -163,10 +157,7 @@ def initialize(query)
163157
return
164158
end
165159
operator = ''
166-
if ['eternal_points', 'global_penalty', 'universal_faction_cost'].include?(keyword)
167-
@left_joins << :unified_restrictions
168-
end
169-
if @@numeric_operators.include?(match_type)
160+
if @@numeric_operators.include?(match_type)
170161
operator = @@numeric_operators[match_type]
171162
else
172163
@parse_error = 'Invalid numeric operator "%s"' % match_type
@@ -184,15 +175,13 @@ def initialize(query)
184175
@parse_error = 'Invalid string operator "%s"' % match_type
185176
return
186177
end
187-
if ['s', 'card_subtype'].include?(keyword)
188-
@left_joins << :card_subtypes
189-
elsif keyword == 'card_pool'
190-
@left_joins << :card_pool_cards
191-
end
192-
constraints << 'lower(%s) %s ?' % [@@term_to_field_map[keyword], operator]
178+
constraints << 'lower(%s) %s ?' % [@@term_to_field_map[keyword], operator]
193179
where << '%%%s%%' % value
194180
end
195-
end
181+
if @@term_to_left_join_map.include?(keyword)
182+
@left_joins << @@term_to_left_join_map[keyword]
183+
end
184+
end
196185

197186
# bare/quoted words in the query are automatically mapped to stripped_title
198187
if f.include?(:string)
@@ -216,6 +205,6 @@ def where_values
216205
return @where_values
217206
end
218207
def left_joins
219-
return @left_joins
208+
return @left_joins.to_a
220209
end
221210
end

lib/printing_search_parser.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'parslet'
2+
3+
# TODO(plural): Add support for | in : and ! operators .
4+
class PrintingSearchParser < Parslet::Parser
5+
rule(:spaces) { match('\s').repeat(1) }
6+
rule(:spaces?) { spaces.maybe }
7+
rule(:bare_string) {
8+
match('[!\w-]').repeat(1).as(:string)
9+
}
10+
rule(:quoted_string) {
11+
str('"') >> (
12+
str('"').absent? >> any
13+
).repeat.as(:string) >> str('"')
14+
}
15+
rule(:string) {
16+
spaces? >> (bare_string | quoted_string) >> spaces?
17+
}
18+
# Note that while this list should generally be kept sorted, an entry that is a prefix of
19+
# a later entry will clobber the later entries and throw an error parsing text with the later entries.
20+
rule(:keyword) {
21+
str('advancement_cost') |
22+
str('agenda_points') |
23+
str('base_link') |
24+
str('card_cycle') |
25+
str('card_pool') |
26+
str('card_set') |
27+
str('card_subtype') |
28+
str('card_type') |
29+
str('cost') |
30+
str('eternal_points') |
31+
str('faction') |
32+
str('flavor') |
33+
str('format') |
34+
str('global_penalty') |
35+
str('illustrator') |
36+
str('influence_cost') |
37+
str('is_banned') |
38+
str('is_restricted') |
39+
str('is_unique') |
40+
str('memory_usage') |
41+
str('quantity') |
42+
str('release_date') |
43+
str('restriction_id') |
44+
str('side') |
45+
str('strength') |
46+
str('text') |
47+
str('title') |
48+
str('trash_cost') |
49+
str('universal_faction_cost') |
50+
# Single letter 'short codes'
51+
match('[_abcdefghilmnoprstuvxyz]')
52+
}
53+
rule(:match_type) { str('<=') | str('>=') | match('[:!<>]') }
54+
rule(:operator) { keyword >> match_type}
55+
rule(:search_term) { keyword.as(:keyword) >> match_type.as(:match_type) >> (string).as(:value) }
56+
rule(:query) {
57+
(spaces? >> (search_term.as(:search_term) | string) >> spaces?).repeat.as(:fragments)
58+
}
59+
root :query
60+
end

0 commit comments

Comments
 (0)