quby-compiler 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +5 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +11 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/config/locales/de.yml +58 -0
- data/config/locales/en.yml +57 -0
- data/config/locales/nl.yml +57 -0
- data/config/locales/rails-i18n/README.md +4 -0
- data/config/locales/rails-i18n/de.yml +223 -0
- data/config/locales/rails-i18n/en.yml +216 -0
- data/config/locales/rails-i18n/nl.yml +214 -0
- data/exe/quby-compile +56 -0
- data/lib/quby/array_attribute_valid_validator.rb +15 -0
- data/lib/quby/attribute_valid_validator.rb +14 -0
- data/lib/quby/compiler.rb +50 -0
- data/lib/quby/compiler/dsl.rb +29 -0
- data/lib/quby/compiler/dsl/base.rb +20 -0
- data/lib/quby/compiler/dsl/calls_custom_methods.rb +29 -0
- data/lib/quby/compiler/dsl/charting/bar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/charting/chart_builder.rb +95 -0
- data/lib/quby/compiler/dsl/charting/line_chart_builder.rb +34 -0
- data/lib/quby/compiler/dsl/charting/overview_chart_builder.rb +31 -0
- data/lib/quby/compiler/dsl/charting/radar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/helpers.rb +53 -0
- data/lib/quby/compiler/dsl/panel_builder.rb +80 -0
- data/lib/quby/compiler/dsl/question_builder.rb +40 -0
- data/lib/quby/compiler/dsl/questionnaire_builder.rb +279 -0
- data/lib/quby/compiler/dsl/questions/base.rb +180 -0
- data/lib/quby/compiler/dsl/questions/checkbox_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/date_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/deprecated_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/float_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/integer_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/radio_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/select_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/string_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/text_question_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_schema_builder.rb +53 -0
- data/lib/quby/compiler/dsl/standardized_panel_generators.rb +33 -0
- data/lib/quby/compiler/dsl/table_builder.rb +48 -0
- data/lib/quby/compiler/entities.rb +38 -0
- data/lib/quby/compiler/entities/charting/bar_chart.rb +17 -0
- data/lib/quby/compiler/entities/charting/chart.rb +101 -0
- data/lib/quby/compiler/entities/charting/charts.rb +42 -0
- data/lib/quby/compiler/entities/charting/line_chart.rb +38 -0
- data/lib/quby/compiler/entities/charting/overview_chart.rb +20 -0
- data/lib/quby/compiler/entities/charting/plottable.rb +20 -0
- data/lib/quby/compiler/entities/charting/radar_chart.rb +17 -0
- data/lib/quby/compiler/entities/definition.rb +26 -0
- data/lib/quby/compiler/entities/fields.rb +119 -0
- data/lib/quby/compiler/entities/flag.rb +55 -0
- data/lib/quby/compiler/entities/item.rb +40 -0
- data/lib/quby/compiler/entities/lookup_tables.rb +71 -0
- data/lib/quby/compiler/entities/outcome_table.rb +31 -0
- data/lib/quby/compiler/entities/panel.rb +82 -0
- data/lib/quby/compiler/entities/question.rb +365 -0
- data/lib/quby/compiler/entities/question_option.rb +96 -0
- data/lib/quby/compiler/entities/questionnaire.rb +440 -0
- data/lib/quby/compiler/entities/questions/checkbox_question.rb +82 -0
- data/lib/quby/compiler/entities/questions/date_question.rb +84 -0
- data/lib/quby/compiler/entities/questions/deprecated_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/float_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/integer_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/radio_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/select_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/string_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/text_question.rb +15 -0
- data/lib/quby/compiler/entities/score_calculation.rb +35 -0
- data/lib/quby/compiler/entities/score_schema.rb +25 -0
- data/lib/quby/compiler/entities/subscore_schema.rb +23 -0
- data/lib/quby/compiler/entities/table.rb +143 -0
- data/lib/quby/compiler/entities/text.rb +71 -0
- data/lib/quby/compiler/entities/textvar.rb +23 -0
- data/lib/quby/compiler/entities/validation.rb +17 -0
- data/lib/quby/compiler/entities/version.rb +23 -0
- data/lib/quby/compiler/entities/visibility_rule.rb +71 -0
- data/lib/quby/compiler/instance.rb +72 -0
- data/lib/quby/compiler/output.rb +13 -0
- data/lib/quby/compiler/outputs.rb +4 -0
- data/lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb +362 -0
- data/lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb +15 -0
- data/lib/quby/compiler/outputs/roqua_serializer.rb +108 -0
- data/lib/quby/compiler/outputs/seed_serializer.rb +34 -0
- data/lib/quby/compiler/services/definition_validator.rb +330 -0
- data/lib/quby/compiler/services/quby_proxy.rb +405 -0
- data/lib/quby/compiler/services/seed_diff.rb +116 -0
- data/lib/quby/compiler/services/text_transformation.rb +30 -0
- data/lib/quby/compiler/version.rb +5 -0
- data/lib/quby/markdown_parser.rb +38 -0
- data/lib/quby/range_categories.rb +38 -0
- data/lib/quby/settings.rb +86 -0
- data/lib/quby/text_transformation.rb +26 -0
- data/lib/quby/type_validator.rb +12 -0
- data/quby-compiler.gemspec +39 -0
- metadata +277 -0
@@ -0,0 +1,405 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
require "active_support/all"
|
3
|
+
require 'roqua/core_ext/enumerable/sort_by_alphanum'
|
4
|
+
require 'quby/compiler/services/seed_diff'
|
5
|
+
|
6
|
+
module Quby
|
7
|
+
module Compiler
|
8
|
+
module Services
|
9
|
+
class QubyProxy
|
10
|
+
HEADERS = { value: "Score",
|
11
|
+
interpretation: "Interpretatie",
|
12
|
+
clin_interp: "Klinisch",
|
13
|
+
norm: "Norm",
|
14
|
+
tscore: "T-Score",
|
15
|
+
dimensie: "Dimensie",
|
16
|
+
mean: "Gemiddelde" }
|
17
|
+
|
18
|
+
attr_reader :questionnaire, :options
|
19
|
+
|
20
|
+
def initialize(questionnaire, options)
|
21
|
+
@questionnaire = questionnaire
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate(seed)
|
26
|
+
question_titles = generate_question_titles
|
27
|
+
d_qtypes = {}
|
28
|
+
vars = []
|
29
|
+
@hidden_questions = {} # hash containing questions hidden by other questions
|
30
|
+
|
31
|
+
for question in questions_flat
|
32
|
+
if question.hidden && question.type != :check_box
|
33
|
+
d_qtypes[question.key.to_s] = { depends: :present } unless options[:without_depends]
|
34
|
+
end
|
35
|
+
unless question.hidden && (question.type == :check_box || question.type == :hidden)
|
36
|
+
vars << question.key.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
case question.type
|
40
|
+
when :radio, :scale
|
41
|
+
handle_scale(question, question_titles, d_qtypes, vars)
|
42
|
+
when :select
|
43
|
+
d_qtypes[question.key.to_s] = { type: :discrete }
|
44
|
+
for option in question.options
|
45
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "") unless option.placeholder
|
46
|
+
end
|
47
|
+
update_hidden_questions_for(question)
|
48
|
+
when :check_box
|
49
|
+
d_qtypes[question.key.to_s] = { type: :check_box }
|
50
|
+
question.options.each do |option|
|
51
|
+
next if option.inner_title
|
52
|
+
vars << option.key.to_s
|
53
|
+
if question.hidden
|
54
|
+
question_titles[option.key.to_s] = strip_tags question.context_free_title
|
55
|
+
end
|
56
|
+
value = 1
|
57
|
+
option_type = { type: :discrete }
|
58
|
+
option_type[value.to_s] = (option.context_free_description || "")
|
59
|
+
option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
|
60
|
+
d_qtypes[option.key.to_s] = option_type
|
61
|
+
values = [value, value.to_s].uniq
|
62
|
+
handle_subquestions(question, question_titles, d_qtypes, vars, option, values, option.key.to_s)
|
63
|
+
end
|
64
|
+
update_hidden_questions_for(question, for_checkbox: true)
|
65
|
+
when :textarea
|
66
|
+
d_qtypes[question.key.to_s] = { type: :text_field }
|
67
|
+
when :string, :integer, :float
|
68
|
+
handle_textfield(question, d_qtypes)
|
69
|
+
when :date
|
70
|
+
d_qtypes[question.key.to_s] = question.components.each_with_object({ type: :date }) do |component, hash|
|
71
|
+
key = question.send("#{component}_key")
|
72
|
+
vars << key.to_s
|
73
|
+
hash[component] = key.to_s
|
74
|
+
end
|
75
|
+
when :hidden
|
76
|
+
if question.options.blank? # string
|
77
|
+
question_titles[question.key.to_s] = strip_tags question.context_free_title
|
78
|
+
vars << question.key.to_s unless vars.include? question.key.to_s
|
79
|
+
d_qtypes[question.key.to_s] = { type: :text }
|
80
|
+
d_qtypes[question.key.to_s][:depends] = :present unless options[:without_depends]
|
81
|
+
else
|
82
|
+
no_keys = true
|
83
|
+
values = []
|
84
|
+
question.options.each do |option|
|
85
|
+
if option.value # scale or radio
|
86
|
+
vars << question.key.to_s unless vars.include? question.key.to_s
|
87
|
+
next if option.inner_title
|
88
|
+
d_qtypes[question.key.to_s] ||= { type: :scale }
|
89
|
+
values << option.value.to_s
|
90
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
91
|
+
# TODO: missing sub-questions
|
92
|
+
else # check_box
|
93
|
+
d_qtypes[question.key.to_s] ||= { type: :check_box }
|
94
|
+
no_keys = false
|
95
|
+
question_titles[option.key.to_s] = strip_tags question.context_free_title
|
96
|
+
vars << option.key.to_s
|
97
|
+
value = option.value || 1
|
98
|
+
option_type = { type: :discrete }
|
99
|
+
option_type[value.to_s] = (option.context_free_description || "")
|
100
|
+
option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
|
101
|
+
d_qtypes[option.key.to_s] = option_type
|
102
|
+
# TODO: missing sub-questions
|
103
|
+
end
|
104
|
+
end
|
105
|
+
if no_keys # scale or radio
|
106
|
+
d_qtypes[question.key.to_s][:depends] = { values: values, variable: question.key.to_s } unless options[:without_depends]
|
107
|
+
question_titles[question.key.to_s] = strip_tags question.context_free_title
|
108
|
+
end
|
109
|
+
end
|
110
|
+
else
|
111
|
+
fail "WARNING: Unimplemented type #{question.type}."
|
112
|
+
end
|
113
|
+
|
114
|
+
update_dqtypes_depends(d_qtypes, question, options)
|
115
|
+
end
|
116
|
+
|
117
|
+
seed["quests"] = sort_nested_hash(question_titles)
|
118
|
+
seed["d_qtypes"] = sort_nested_hash(d_qtypes)
|
119
|
+
seed["name"] = questionnaire.title
|
120
|
+
seed["short_description"] = questionnaire.short_description unless questionnaire.short_description.blank?
|
121
|
+
seed["description"] = questionnaire.description unless questionnaire.description.blank?
|
122
|
+
|
123
|
+
# this approach preserves the order of vars as much as possible, adding new vars to the end of the list
|
124
|
+
old_vars = (seed["vars"]&.split(",") || []).map(&:to_s)
|
125
|
+
new_vars = vars.map(&:to_s)
|
126
|
+
seed["vars"] = ((old_vars & new_vars) | new_vars).join(",")
|
127
|
+
|
128
|
+
scores = process_scores
|
129
|
+
|
130
|
+
seed["properties"] ||= {}
|
131
|
+
# headers outcome (humanized)
|
132
|
+
seed["properties"][:score_headers] = scores[:headers]
|
133
|
+
# headers data-export
|
134
|
+
seed["properties"][:score_keys] = scores[:keys]
|
135
|
+
# score names outcome (humanized)
|
136
|
+
seed["properties"][:score_labels] = scores[:labels]
|
137
|
+
|
138
|
+
seed["properties"].merge!(@options[:properties]) if @options.key?(:properties)
|
139
|
+
seed["properties"] = sort_nested_hash(seed["properties"])
|
140
|
+
|
141
|
+
data = {"key" => seed["key"] || options[:roqua_key] || questionnaire.key, "remote_id" => questionnaire.key}
|
142
|
+
attrs = %w(name vars quests d_qtypes properties short_description)
|
143
|
+
attrs.sort.each do |name|
|
144
|
+
data[name] = seed[name]
|
145
|
+
end
|
146
|
+
|
147
|
+
data
|
148
|
+
end
|
149
|
+
|
150
|
+
def update_hidden_questions_for(question, for_checkbox: false)
|
151
|
+
shows = question.options.each_with_object({}) do |option, shows|
|
152
|
+
next if option.inner_title
|
153
|
+
for key in option.shows_questions
|
154
|
+
skey = key.to_s
|
155
|
+
if for_checkbox
|
156
|
+
# is another checkbox option already showing the target question?
|
157
|
+
if shows.key?(skey)
|
158
|
+
# then set the target's depends on :present, since we cannot represent depending on multiple variables
|
159
|
+
shows[skey] = :present
|
160
|
+
else
|
161
|
+
shows[skey] = { values: ["1", 1], variable: option.key.to_s }
|
162
|
+
end
|
163
|
+
else
|
164
|
+
shows[skey] ||= { values: [], variable: question.key.to_s }
|
165
|
+
shows[skey][:values] |= [option.value.to_s, option.value]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
for skey, show in shows
|
170
|
+
# if a different question is already showing the same question, we cannot register a dependency on both questions
|
171
|
+
# (the 'variable' attribute accepts only 1 key). Thus it is better to show the question based on presence of
|
172
|
+
# an answer instead of on the depended question's answers.
|
173
|
+
if @hidden_questions.has_key?(skey)
|
174
|
+
@hidden_questions[skey] = :present
|
175
|
+
else
|
176
|
+
@hidden_questions[skey] = show
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def update_dqtypes_depends(d_qtypes, question, options)
|
182
|
+
if hidden = @hidden_questions[question.key.to_s]
|
183
|
+
d_qtypes[question.key.to_s][:depends] ||= hidden unless options[:without_depends]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def generate_question_titles
|
188
|
+
question_titles = {}
|
189
|
+
|
190
|
+
for question in questions_flat
|
191
|
+
unless question.hidden && (question.type == :check_box || question.type == :hidden)
|
192
|
+
title = question.context_free_title || question.description || ""
|
193
|
+
question_titles[question.key.to_s] = strip_tags(title)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
question_titles
|
198
|
+
end
|
199
|
+
|
200
|
+
def questions_flat
|
201
|
+
@questions_flat ||= questionnaire.panels.map do |panel|
|
202
|
+
panel.items.select { |item| item.is_a? Quby::Compiler::Entities::Question }
|
203
|
+
end.flatten.compact
|
204
|
+
end
|
205
|
+
|
206
|
+
def handle_subquestions(question, quests, d_qtypes, vars, option, values, key)
|
207
|
+
option.questions.each do |quest|
|
208
|
+
if quest.presentation == :next_to_title && ![:string, :integer, :float].include?(quest.type)
|
209
|
+
fail "unsupported title question type"
|
210
|
+
end
|
211
|
+
case quest.type
|
212
|
+
when :string, :integer, :float
|
213
|
+
subquestion(question, quests, d_qtypes, vars, quest, values, key)
|
214
|
+
when :textarea
|
215
|
+
sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
|
216
|
+
when :radio
|
217
|
+
sub_radio(question, quests, d_qtypes, vars, quest, values, key)
|
218
|
+
when :date
|
219
|
+
sub_date(question, quests, d_qtypes, vars, quest, values, key)
|
220
|
+
else
|
221
|
+
fail "Unimplemented type #{quest.type} for sub_question"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def subquestion(question, quests, d_qtypes, vars, quest, values, key)
|
227
|
+
d_qtypes[quest.key.to_s] = { type: :text }
|
228
|
+
unless options[:without_depends]
|
229
|
+
if quest.presentation == :next_to_title
|
230
|
+
# make title questons dependent on themselves so we don't have to dig into quby's depends relations
|
231
|
+
# which sometimes refer to some of the parent's options, but not always the correct ones
|
232
|
+
d_qtypes[quest.key.to_s][:depends] = :present
|
233
|
+
else
|
234
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
d_qtypes[quest.key.to_s][:label] = quest.unit unless quest.unit.blank?
|
238
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
239
|
+
vars << quest.key.to_s
|
240
|
+
end
|
241
|
+
|
242
|
+
def sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
|
243
|
+
d_qtypes[quest.key.to_s] = { type: :text_field }
|
244
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
|
245
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
246
|
+
vars << quest.key.to_s
|
247
|
+
end
|
248
|
+
|
249
|
+
def sub_radio(question, quests, d_qtypes, vars, quest, values, key)
|
250
|
+
d_qtypes[quest.key.to_s] = { type: :scale }
|
251
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
|
252
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
253
|
+
for option in quest.options
|
254
|
+
next if option.inner_title
|
255
|
+
d_qtypes[quest.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
256
|
+
end
|
257
|
+
vars << quest.key.to_s
|
258
|
+
update_hidden_questions_for(quest)
|
259
|
+
end
|
260
|
+
|
261
|
+
def sub_date(question, quests, d_qtypes, vars, quest, values, key)
|
262
|
+
d_qtypes[quest.key.to_s] = quest.components.each_with_object({ type: :date }) do |component, hash|
|
263
|
+
key = quest.send("#{component}_key")
|
264
|
+
vars << key
|
265
|
+
hash[component] = key.to_s
|
266
|
+
end
|
267
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
268
|
+
end
|
269
|
+
|
270
|
+
def handle_scale(question, quests, d_qtypes, vars)
|
271
|
+
d_qtypes[question.key.to_s] = { type: :scale }
|
272
|
+
values = []
|
273
|
+
update_hidden_questions_for(question)
|
274
|
+
for option in question.options
|
275
|
+
next if option.inner_title
|
276
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
277
|
+
values << option.value.to_s
|
278
|
+
key = question.key.to_s
|
279
|
+
handle_subquestions(question, quests, d_qtypes, vars, option, [option.value.to_s], key)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def handle_textfield(question, d_qtypes)
|
284
|
+
d_qtypes[question.key.to_s] = { type: :text }
|
285
|
+
d_qtypes[question.key.to_s][:label] = question.unit unless question.unit.blank?
|
286
|
+
end
|
287
|
+
|
288
|
+
def strip_p_tag(text)
|
289
|
+
text.gsub(/^<p>(.*)<\/p>\n?$/, "\\1")
|
290
|
+
end
|
291
|
+
|
292
|
+
def process_scores
|
293
|
+
scores_from_schemas
|
294
|
+
end
|
295
|
+
|
296
|
+
def scores_from_schemas
|
297
|
+
score_headers = [] # headers outcome (humanized name for subscores)
|
298
|
+
score_keys = [] # headers data-export (not all of it, just the score_subscore part, shortened)
|
299
|
+
score_labels = [] # score names outcome (humanized name for score as a whole)
|
300
|
+
|
301
|
+
questionnaire.score_schemas.values.each do |score_schema|
|
302
|
+
score_labels << score_schema.label
|
303
|
+
score_keys << score_schema.subscore_schemas.map do |subschema|
|
304
|
+
hash = {
|
305
|
+
key: subschema.key,
|
306
|
+
header: subschema.export_key.to_s # a shortened key used as PART OF the csv export column headers
|
307
|
+
}
|
308
|
+
if subschema.only_for_export
|
309
|
+
hash.merge(hidden: true)
|
310
|
+
else
|
311
|
+
hash
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
headers = score_schema.subscore_schemas.map(&:label)
|
316
|
+
score_headers += headers - score_headers
|
317
|
+
end
|
318
|
+
|
319
|
+
{
|
320
|
+
headers: score_headers,
|
321
|
+
keys: score_keys,
|
322
|
+
labels: score_labels
|
323
|
+
}
|
324
|
+
end
|
325
|
+
|
326
|
+
class ShortenKeysUniq
|
327
|
+
def initialize
|
328
|
+
@seen_results = []
|
329
|
+
end
|
330
|
+
|
331
|
+
def shorten_one(key)
|
332
|
+
key = key.to_s
|
333
|
+
limit = 2
|
334
|
+
shortened_key = nil
|
335
|
+
loop do
|
336
|
+
shortened_key = key[0..limit]
|
337
|
+
if key[limit] == "_"
|
338
|
+
limit += 1
|
339
|
+
next
|
340
|
+
end
|
341
|
+
break unless @seen_results.include?(shortened_key)
|
342
|
+
raise "duplicate key, #{key}" if shortened_key.length == key.length
|
343
|
+
limit += 1
|
344
|
+
end
|
345
|
+
|
346
|
+
@seen_results << shortened_key
|
347
|
+
shortened_key
|
348
|
+
end
|
349
|
+
|
350
|
+
def shorten_two(first_key, second_key)
|
351
|
+
first_key = first_key.to_s
|
352
|
+
second_key = second_key.to_s
|
353
|
+
first_limit = [2, first_key.length - 1].min
|
354
|
+
second_limit = 0
|
355
|
+
shortened_key = nil
|
356
|
+
loop do
|
357
|
+
shortened_key = "#{first_key[0..first_limit]}_#{second_key[0..second_limit]}"
|
358
|
+
if first_key[first_limit] == "_"
|
359
|
+
first_limit += 1
|
360
|
+
next
|
361
|
+
end
|
362
|
+
if second_key[second_limit] == "_"
|
363
|
+
second_limit += 1
|
364
|
+
next
|
365
|
+
end
|
366
|
+
break unless @seen_results.include?(shortened_key)
|
367
|
+
raise "duplicate key, #{first_key}_#{second_key}" if first_limit == (first_key.length - 1) &&
|
368
|
+
second_limit == (second_key.length - 1)
|
369
|
+
|
370
|
+
if second_limit == (second_key.length - 1)
|
371
|
+
first_limit += 1
|
372
|
+
second_limit = 0
|
373
|
+
else
|
374
|
+
second_limit += 1
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
@seen_results << shortened_key
|
379
|
+
shortened_key
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def sort_nested_hash(obj)
|
384
|
+
case obj
|
385
|
+
when Hash
|
386
|
+
obj.transform_values { |v| sort_nested_hash(v) }
|
387
|
+
.sort_by_alphanum { |k, _v| k.to_s }
|
388
|
+
.to_h
|
389
|
+
when Array
|
390
|
+
obj.map { |v| sort_nested_hash(v) }
|
391
|
+
else
|
392
|
+
obj
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
def self.keys_for_score(score)
|
400
|
+
score.map { |subscore| subscore[:key] }
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Quby
|
2
|
+
module Compiler
|
3
|
+
module Services
|
4
|
+
class SeedDiff
|
5
|
+
def format_patch(reference, candidate, path:)
|
6
|
+
@reference_for_debugging = reference
|
7
|
+
@candidate_for_debugging = candidate
|
8
|
+
format_polymorph_patch(reference, candidate, path: path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply_patch(object, patch)
|
12
|
+
return object if patch.nil?
|
13
|
+
apply_polymorphic(object, patch, path: "")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def format_polymorph_patch(reference, candidate, path:)
|
19
|
+
if reference.class != candidate.class && !candidate.nil? && !depends_relation?(path)
|
20
|
+
raise "Incompatible types at #{path}: #{reference.class} and #{candidate.class}"
|
21
|
+
end
|
22
|
+
|
23
|
+
case reference
|
24
|
+
when Hash
|
25
|
+
case candidate
|
26
|
+
when Hash
|
27
|
+
format_hash_patch(reference, candidate, path: path)
|
28
|
+
else
|
29
|
+
reference
|
30
|
+
end
|
31
|
+
when Array
|
32
|
+
format_array_patch(reference, candidate, path: path)
|
33
|
+
when String, Symbol, true, false
|
34
|
+
reference
|
35
|
+
else
|
36
|
+
raise "Cannot patch #{reference.class} yet."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def format_hash_patch(reference, candidate, path:)
|
41
|
+
return reference if candidate.nil?
|
42
|
+
|
43
|
+
patch = {}
|
44
|
+
|
45
|
+
reference.keys.each do |key|
|
46
|
+
if reference[key] != candidate[key]
|
47
|
+
patch[key] = format_polymorph_patch(reference[key], candidate[key], path: path + ".#{key}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
patch
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_array_patch(reference, candidate, path:)
|
55
|
+
return reference if candidate.nil?
|
56
|
+
|
57
|
+
patch = { __patch_type__: "array" }
|
58
|
+
|
59
|
+
reference.each_with_index do |reference_elm, idx|
|
60
|
+
if reference_elm != candidate[idx]
|
61
|
+
patch[idx] = reference_elm
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if candidate.size > reference.size
|
66
|
+
patch[:__patch_trim__] = reference.size
|
67
|
+
end
|
68
|
+
|
69
|
+
patch
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_polymorphic(object, patch, path:)
|
73
|
+
if patch.is_a?(Hash) && patch[:__patch_type__] == "array"
|
74
|
+
apply_array(object, patch, path: path)
|
75
|
+
elsif patch.is_a?(Hash)
|
76
|
+
apply_hash(object, patch, path: path)
|
77
|
+
else
|
78
|
+
patch
|
79
|
+
end
|
80
|
+
rescue Exception => e
|
81
|
+
puts "path: #{path}"
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
|
85
|
+
def apply_hash(object, patch, path:)
|
86
|
+
object = {} unless object.is_a?(Hash)
|
87
|
+
|
88
|
+
patch.each do |key, value|
|
89
|
+
object[key] = apply_polymorphic(object[key], value, path: path + ".#{key}")
|
90
|
+
end
|
91
|
+
|
92
|
+
object
|
93
|
+
end
|
94
|
+
|
95
|
+
def apply_array(object, patch, path:)
|
96
|
+
object ||= []
|
97
|
+
|
98
|
+
patch.each do |key, val|
|
99
|
+
next if key == :__patch_type__ || key == :__patch_trim__
|
100
|
+
object[key] = val
|
101
|
+
end
|
102
|
+
|
103
|
+
if patch[:__patch_trim__]
|
104
|
+
object = object.slice(0, patch[:__patch_trim__])
|
105
|
+
end
|
106
|
+
|
107
|
+
object
|
108
|
+
end
|
109
|
+
|
110
|
+
def depends_relation?(path)
|
111
|
+
path.end_with?(".depends")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|