praxis 2.0.pre.13 → 2.0.pre.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +81 -23
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +3 -4
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +52 -12
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +110 -35
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +25 -3
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +13 -5
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/spec/spec_helper.rb +1 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dafcffa2e0d146f5b601ed796cb482c5068482757fc2f88770783536da52ad7e
|
4
|
+
data.tar.gz: c6f8875641b0e418128dd8f8a923a3df639b1a409e65976472c24b6fac52f90e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4728bf36f52fd0bf1f73c9efe02190cde40308be2378232897570deab1aa9e5fcbd1ca2b4335b9ad168c4eedda18143a512512e6c132d851b6f6c9447e891d19
|
7
|
+
data.tar.gz: 1d689d714604b4841f4059b1ef9734f1ceae0a9afcc7e7d75286be28f67cfe2a51cb86fafdd6c66b2ac40acc72bd431c12ad1b33aa91990ceecd690abc9e01ad
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,25 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.17
|
6
|
+
* Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
|
7
|
+
* Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
|
8
|
+
there are no associated rows (`!!`) or if there are some associated rows (`!`)
|
9
|
+
* Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
|
10
|
+
## 2.0.pre.16
|
11
|
+
|
12
|
+
* Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
|
13
|
+
* Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
|
14
|
+
|
15
|
+
## 2.0.pre.14
|
16
|
+
|
17
|
+
* More encoding/decoding robustness for filters.
|
18
|
+
* Specs for how to encode filters are now properly defined by:
|
19
|
+
* The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
|
20
|
+
* The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
|
21
|
+
* When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
|
22
|
+
* Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
|
23
|
+
|
5
24
|
## 2.0.pre.13
|
6
25
|
|
7
26
|
* Fix filters parser regression, which would incorrectly decode url-encoded values
|
data/bin/praxis
CHANGED
@@ -9,6 +9,12 @@ rescue Bundler::GemfileNotFound
|
|
9
9
|
# no-op: we might be installed as a system gem
|
10
10
|
end
|
11
11
|
|
12
|
+
if ARGV[0] == "version"
|
13
|
+
require 'praxis/version'
|
14
|
+
puts "Praxis version #{Praxis::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
12
18
|
if ["routes","docs","console"].include? ARGV[0]
|
13
19
|
require 'rake'
|
14
20
|
require 'praxis'
|
@@ -97,8 +97,10 @@ module Praxis
|
|
97
97
|
description( description || 'Standard response for successful HTTP requests.' )
|
98
98
|
|
99
99
|
media_type media_type
|
100
|
-
location location
|
101
|
-
headers
|
100
|
+
location if location
|
101
|
+
headers&.each do |(name, value)|
|
102
|
+
header(name: name, value: value)
|
103
|
+
end
|
102
104
|
end
|
103
105
|
|
104
106
|
api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
|
@@ -106,8 +108,10 @@ module Praxis
|
|
106
108
|
description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
|
107
109
|
|
108
110
|
media_type media_type if media_type
|
109
|
-
location location
|
110
|
-
headers
|
111
|
+
location if location
|
112
|
+
headers&.each do |(name, value)|
|
113
|
+
header(name: name, value: value)
|
114
|
+
end
|
111
115
|
end
|
112
116
|
end
|
113
117
|
|
data/lib/praxis/collection.rb
CHANGED
@@ -32,5 +32,16 @@ module Praxis
|
|
32
32
|
@member_type.domain_model
|
33
33
|
end
|
34
34
|
|
35
|
+
def self.json_schema_type
|
36
|
+
:array
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.as_json_schema(**args)
|
40
|
+
the_type = @attribute && @attribute.type || member_type
|
41
|
+
{
|
42
|
+
type: json_schema_type,
|
43
|
+
items: { '$ref': "#/components/schemas/#{the_type.id}" }
|
44
|
+
}
|
45
|
+
end
|
35
46
|
end
|
36
47
|
end
|
@@ -16,12 +16,27 @@ module Praxis
|
|
16
16
|
|
17
17
|
def dump_response_headers_object( headers )
|
18
18
|
headers.each_with_object({}) do |(name,data),accum|
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# each header comes from Praxis::ResponseDefinition
|
20
|
+
# the keys are the header names, and value can be:
|
21
|
+
# "true" => means it only needs to exist
|
22
|
+
# String => which means that it has to fully match
|
23
|
+
# Regex => which means it has to regexp match it
|
24
|
+
|
25
|
+
# Get the schema from the type (defaulting to string in case the type doesn't have the as_json_schema defined)
|
26
|
+
schema = data[:attribute].type.as_json_schema rescue { type: :string }
|
27
|
+
hash = { description: data[:description] || '', schema: schema }
|
28
|
+
# Note, our Headers in response definition are not full types...they're basically only
|
29
|
+
# strings, which can either match anything, match the exact word or match a regex
|
30
|
+
# they don't even have a description...
|
31
|
+
data_value = data[:value]
|
32
|
+
if data_value.is_a? String
|
33
|
+
hash[:pattern] = "^#{data_value}$" # Exact String match
|
34
|
+
elsif data_value.is_a? Regexp
|
35
|
+
sanitized_pattern = data_value.inspect[1..-2] #inspect returns enclosing '/' characters
|
36
|
+
hash[:pattern] = sanitized_pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
accum[name] = hash
|
25
40
|
end
|
26
41
|
end
|
27
42
|
|
@@ -1,2 +1,15 @@
|
|
1
1
|
require 'praxis/extensions/attribute_filtering/filtering_params'
|
2
|
-
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
2
|
+
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
module AttributeFiltering
|
6
|
+
class MultiMatchWithFuzzyNotAllowedByAdapter < StandardError
|
7
|
+
def initialize
|
8
|
+
msg = 'Matching multiple, comma-separated values with fuzzy matches for a single field is not allowed by this DB adapter'\
|
9
|
+
'Please use multiple OR clauses instead.'
|
10
|
+
super(msg)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -23,7 +23,8 @@ module Praxis
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class ActiveRecordFilterQueryBuilder
|
26
|
-
|
26
|
+
REFERENCES_STRING_SEPARATOR = '/'
|
27
|
+
attr_reader :model, :filters_map
|
27
28
|
|
28
29
|
# Base query to build upon
|
29
30
|
def initialize(query: , model:, filters_map:, debug: false)
|
@@ -48,9 +49,8 @@ module Praxis
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def craft_filter_query(nodetree, for_model:)
|
51
|
-
result = _compute_joins_and_conditions_data(nodetree, model: for_model)
|
52
|
+
result = _compute_joins_and_conditions_data(nodetree, model: for_model, parent_reflection: nil)
|
52
53
|
return @initial_query if result[:conditions].empty?
|
53
|
-
|
54
54
|
|
55
55
|
# Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
|
56
56
|
root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
|
@@ -59,20 +59,35 @@ module Praxis
|
|
59
59
|
end
|
60
60
|
|
61
61
|
# Process the joins
|
62
|
-
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.
|
62
|
+
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.left_outer_joins(result[:associations_hash])
|
63
63
|
|
64
64
|
# Proc to apply a single condition
|
65
65
|
apply_single_condition = Proc.new do |condition, associated_query|
|
66
66
|
colo = condition[:model].columns_hash[condition[:name].to_s]
|
67
67
|
column_prefix = condition[:column_prefix]
|
68
|
-
|
69
|
-
|
68
|
+
association_key_column = \
|
69
|
+
if ref = condition[:parent_reflection]
|
70
|
+
# get the target model of the association(where the assoc pk is)
|
71
|
+
target_model = condition[:parent_reflection].klass
|
72
|
+
target_model.columns_hash[condition[:parent_reflection].association_primary_key]
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
|
78
|
+
# If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
|
79
|
+
# unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
|
80
|
+
unless for_model.table_name == column_prefix
|
81
|
+
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
|
82
|
+
end
|
70
83
|
self.class.add_clause(
|
71
84
|
query: associated_query,
|
72
85
|
column_prefix: column_prefix,
|
73
86
|
column_object: colo,
|
74
87
|
op: condition[:op],
|
75
|
-
value: condition[:value]
|
88
|
+
value: condition[:value],
|
89
|
+
fuzzy: condition[:fuzzy],
|
90
|
+
association_key_column: association_key_column,
|
76
91
|
)
|
77
92
|
end
|
78
93
|
|
@@ -137,7 +152,8 @@ module Praxis
|
|
137
152
|
def _mapped_filter(name)
|
138
153
|
target = @filters_map[name]
|
139
154
|
unless target
|
140
|
-
|
155
|
+
filter_name = name.to_s
|
156
|
+
if (@model.attribute_names + @model.reflections.keys).include?(filter_name)
|
141
157
|
# Cache it in the filters mapping (to avoid later lookups), and return it.
|
142
158
|
@filters_map[name] = name
|
143
159
|
target = name
|
@@ -175,32 +191,64 @@ module Praxis
|
|
175
191
|
end
|
176
192
|
|
177
193
|
# Calculate join tree and conditions array for the nodetree object and its children
|
178
|
-
def _compute_joins_and_conditions_data(nodetree, model:)
|
194
|
+
def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
|
179
195
|
h = {}
|
180
196
|
conditions = []
|
181
197
|
nodetree.children.each do |name, child|
|
182
|
-
|
183
|
-
result = _compute_joins_and_conditions_data(child, model:
|
184
|
-
h[name] = result[:associations_hash]
|
198
|
+
child_reflection = model.reflections[name.to_s]
|
199
|
+
result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
|
200
|
+
h[name] = result[:associations_hash]
|
201
|
+
|
185
202
|
conditions += result[:conditions]
|
186
203
|
end
|
187
|
-
|
188
|
-
|
204
|
+
|
205
|
+
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
|
189
206
|
nodetree.conditions.each do |condition|
|
190
|
-
|
207
|
+
# If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
|
208
|
+
# on the existence (or lack of) of the whole associated table
|
209
|
+
ref = model.reflections[condition[:name].to_s]
|
210
|
+
if ref && ['!','!!'].include?(condition[:op])
|
211
|
+
cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
|
212
|
+
conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
|
213
|
+
h[condition[:name]] = {}
|
214
|
+
else
|
215
|
+
# Save the parent reflection where the condition applies as well (used later to get assoc keys)
|
216
|
+
conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
|
217
|
+
end
|
218
|
+
|
191
219
|
end
|
192
220
|
{associations_hash: h, conditions: conditions}
|
193
221
|
end
|
194
222
|
|
195
|
-
def self.add_clause(query:, column_prefix:, column_object:, op:, value:)
|
196
|
-
likeval = get_like_value(value)
|
223
|
+
def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:, association_key_column:)
|
224
|
+
likeval = get_like_value(value,fuzzy)
|
225
|
+
|
226
|
+
association_op = nil
|
197
227
|
case op
|
198
228
|
when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
|
199
229
|
op = '!='
|
200
230
|
value = nil # Enforce it is indeed nil (should be)
|
231
|
+
association_op = :not_null if association_key_column && !column_object
|
201
232
|
when '!!'
|
202
233
|
op = '='
|
203
234
|
value = nil # Enforce it is indeed nil (should be)
|
235
|
+
association_op = :null if association_key_column && !column_object
|
236
|
+
end
|
237
|
+
|
238
|
+
if association_op
|
239
|
+
neg = association_op == :not_null ? true : false
|
240
|
+
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
|
241
|
+
return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
|
242
|
+
end
|
243
|
+
|
244
|
+
# Add an AND along with the condition, which ensures the left outter join 'exists' for it
|
245
|
+
# Normally this wouldn't be necessary as a condition on a given value mathing would imply the related row was there
|
246
|
+
# but this is not the case for NULL conditions, as the foreign column would match a NULL value, but not because the related column
|
247
|
+
# is NULL, but because the whole missing related row would appear with all fields null
|
248
|
+
# NOTE: we don't need to do it for conditions applying to the root of the tree (there isn't a join to it)
|
249
|
+
if association_key_column
|
250
|
+
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: true)
|
251
|
+
query = query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
|
204
252
|
end
|
205
253
|
|
206
254
|
case op
|
@@ -265,12 +313,22 @@ module Praxis
|
|
265
313
|
end
|
266
314
|
|
267
315
|
# Returns nil if the value was not a fuzzzy pattern
|
268
|
-
def self.get_like_value(value)
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
316
|
+
def self.get_like_value(value,fuzzy)
|
317
|
+
is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
|
318
|
+
if is_fuzzy
|
319
|
+
unless value.is_a?(String)
|
320
|
+
raise MultiMatchWithFuzzyNotAllowedByAdapter.new
|
321
|
+
end
|
322
|
+
case fuzzy
|
323
|
+
when :start_end
|
324
|
+
'%'+value+'%'
|
325
|
+
when :start
|
326
|
+
'%'+value
|
327
|
+
when :end
|
328
|
+
value+'%'
|
329
|
+
end
|
330
|
+
else
|
331
|
+
nil
|
274
332
|
end
|
275
333
|
end
|
276
334
|
|
@@ -15,7 +15,7 @@ module Praxis
|
|
15
15
|
if components.empty?
|
16
16
|
return
|
17
17
|
elsif components.size == 1
|
18
|
-
@conditions << hash.slice(:name, :op, :value, :node_object)
|
18
|
+
@conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
|
19
19
|
else
|
20
20
|
children_data[components.first] ||= []
|
21
21
|
children_data[components.first] << hash
|
@@ -182,7 +182,7 @@ module Praxis
|
|
182
182
|
else
|
183
183
|
spec[:values]
|
184
184
|
end
|
185
|
-
accum.push(name: attr_name, op: spec[:op], value: coerced , node_object: spec[:node_object])
|
185
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
|
186
186
|
end
|
187
187
|
new(accum)
|
188
188
|
end
|
@@ -222,10 +222,9 @@ module Praxis
|
|
222
222
|
value_type = attr_filters[:value_type]
|
223
223
|
next unless value_type == Attributor::String
|
224
224
|
|
225
|
-
|
226
|
-
unless value.empty?
|
225
|
+
if item[:value].presence
|
227
226
|
fuzzy_match = attr_filters[:fuzzy_match]
|
228
|
-
if
|
227
|
+
if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
229
228
|
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
|
230
229
|
end
|
231
230
|
end
|
@@ -15,41 +15,81 @@ module Praxis
|
|
15
15
|
# Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
|
16
16
|
def initialize(triad:, parent_group:)
|
17
17
|
@parent_group = parent_group
|
18
|
-
|
19
18
|
if triad.is_a? Array # several values coming in
|
20
19
|
spec, *values = triad
|
21
20
|
@name = spec[:name].to_sym
|
22
21
|
@op = spec[:op].to_s
|
23
22
|
|
24
|
-
|
25
|
-
""
|
23
|
+
if values.empty?
|
24
|
+
@values = ""
|
25
|
+
@fuzzies = nil
|
26
26
|
elsif values.size == 1
|
27
|
-
|
27
|
+
raw_val = values.first[:value].to_s
|
28
|
+
@values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
|
28
29
|
else
|
29
|
-
values
|
30
|
+
@values = []
|
31
|
+
@fuzzies = []
|
32
|
+
results = values.each do|e|
|
33
|
+
val, fuz = _compute_fuzzy(e[:value].to_s)
|
34
|
+
@values.push val
|
35
|
+
@fuzzies.push fuz
|
36
|
+
end
|
30
37
|
end
|
31
38
|
else # No values for the operand
|
32
39
|
@name = triad[:name].to_sym
|
33
40
|
@op = triad[:op].to_s
|
34
41
|
if ['!','!!'].include?(@op)
|
35
|
-
@values = nil
|
42
|
+
@values, @fuzzies = [nil, nil]
|
36
43
|
else
|
37
44
|
# Value operand without value? => convert it to empty string
|
38
45
|
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
39
|
-
|
46
|
+
if triad[:value] == []
|
47
|
+
@values, @fuzzies = ['', nil]
|
48
|
+
else
|
49
|
+
@values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
|
50
|
+
end
|
40
51
|
end
|
41
52
|
end
|
42
53
|
end
|
43
|
-
|
54
|
+
# Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
|
55
|
+
def _compute_fuzzy(raw_val)
|
56
|
+
starting = raw_val[0] == '*'
|
57
|
+
ending = raw_val[-1] == '*'
|
58
|
+
newval, fuzzy = if starting && ending
|
59
|
+
[raw_val[1..-2], :start_end]
|
60
|
+
elsif starting
|
61
|
+
[raw_val[1..-1], :start]
|
62
|
+
elsif ending
|
63
|
+
[raw_val[0..-2], :end]
|
64
|
+
else
|
65
|
+
[raw_val,nil]
|
66
|
+
end
|
67
|
+
newval = CGI.unescape(newval) if newval
|
68
|
+
[newval,fuzzy]
|
69
|
+
end
|
44
70
|
def flattened_conditions
|
45
|
-
[{name: @name, op: @op, values: @values, node_object: self}]
|
71
|
+
[{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
|
46
72
|
end
|
47
73
|
|
74
|
+
# Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
|
75
|
+
def _dump_value(val,fuzzy)
|
76
|
+
case fuzzy
|
77
|
+
when nil
|
78
|
+
val
|
79
|
+
when :start_end
|
80
|
+
'{*}' + val + '{*}'
|
81
|
+
when :start
|
82
|
+
'{*}' + val
|
83
|
+
when :end
|
84
|
+
val +'{*}'
|
85
|
+
end
|
86
|
+
end
|
48
87
|
def dump
|
49
88
|
vals = if values.is_a? Array
|
50
|
-
|
89
|
+
dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
|
90
|
+
"[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
51
91
|
else
|
52
|
-
(values == '') ? "
|
92
|
+
(values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
53
93
|
end
|
54
94
|
"#{name}#{op}#{vals}"
|
55
95
|
end
|
@@ -134,7 +174,7 @@ module Praxis
|
|
134
174
|
end
|
135
175
|
|
136
176
|
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
137
|
-
rule(:chars) { match('[^&|),]').repeat(0).as(:value) }
|
177
|
+
rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
|
138
178
|
rule(:value) { chars >> (comma >> chars ).repeat }
|
139
179
|
|
140
180
|
rule(:triad) {
|