quby-compiler 0.5.28 → 0.5.30

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 258b02451b98e7d37bda66a027d936e29dc81de2ec230e6cb304e957aa707daa
4
- data.tar.gz: 32afc3c10ac8dc491dea798046ae4175e0c71cdcf31137dc22c822bdf81d5c8b
3
+ metadata.gz: 8a6a65058f64b3e68de9d841447cb82483d112ebf3bb79ea081b8d70369568af
4
+ data.tar.gz: '03487522a9cfe65ffeeddab48f4edb64b6c3f8fa08c5e2b1d5563bcad1ec4de2'
5
5
  SHA512:
6
- metadata.gz: 71caf12ba67af3315949989b6b37213168cb5c18ea1ee2199668ed10c91123af67e23304b55bedbea6522acf89af939b47d3891b1aa24c47197aa7b86aea9fb2
7
- data.tar.gz: c52b9f16bdbf8bd70094dd6d51ec7e29890a55f24e15adb8e727ae42cb4df85db1b3ebc69bf238ec09974c9bab14615c49d02f884b4d96073ab8e7f50ebcf0a6
6
+ metadata.gz: 916e3ba77d76eac79f56c7c6ebf785fac439e12decfbdc3a9b8b60649fa5759cf1dc41df999cf30f122c6a0287800b7f33f309e7bc761eb9fab81ab2a1f63476
7
+ data.tar.gz: 5c010854d019d877a7c9e85986e022229fe78b85f45721cd1adaa535b8023bd73926fd804e927aa19d825262bef8952811d2a7a4aa82988d4cf49e0286711f09
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
+ # 0.5.30
2
+
3
+ * Added support for optgroups under select questions for quby2 (flattened for roqua/quby jsons)
4
+ * Added comparing and boolean logic to sexp_variables. Add type to sexp_variables.
5
+
6
+ # 0.5.29
7
+
8
+ * quby2.json: force scale questions to horizontal (so quby2 doesn't have to anymore)
9
+
1
10
  # 0.5.28
2
11
 
3
- * roqua.json: Don't parse markdown in title/descriptions for layour_version :v2
12
+ * roqua.json: Don't parse markdown in title/descriptions for layout_version :v2
4
13
 
5
14
  # 0.5.27
6
15
 
@@ -212,8 +212,8 @@ module Quby
212
212
  end
213
213
  end
214
214
 
215
- def sexp_variable(key, &block)
216
- @questionnaire.add_sexp_variable(key, SexpVariableBuilder.new(key, &block).build)
215
+ def sexp_variable(key, type: :number, &block)
216
+ @questionnaire.add_sexp_variable(key, SexpVariableBuilder.new(key, type:, &block).build)
217
217
  end
218
218
 
219
219
  # variable :totaal do
@@ -5,12 +5,33 @@ module Quby
5
5
  module DSL
6
6
  module Questions
7
7
  class SelectQuestionBuilder < Base
8
- include MultipleChoice
9
8
  include CompareVisibilityRule
10
9
 
11
10
  def initialize(key, **options, &block)
12
11
  super
13
12
  @question = Entities::Questions::SelectQuestion.new(key, options)
13
+ @current_optgroup = nil
14
+ end
15
+
16
+ def optgroup(key, label: , &block)
17
+ raise "Cannot nest optgroups" if @current_optgroup
18
+ @question.options << @current_optgroup = Entities::QuestionOptgroup.new(key, label:)
19
+ instance_eval(&block)
20
+ @current_optgroup = nil
21
+ end
22
+
23
+ def option(key, options = {}, &block)
24
+ question_option = Entities::QuestionOption.new(key, @question, options)
25
+ if @questionnaire.key_in_use?(question_option.input_key) || @question.key_in_use?(question_option.input_key)
26
+ fail "#{questionnaire.key}:#{@question.key}:#{question_option.key}: " \
27
+ "A question or option with input key #{question_option.input_key} is already defined."
28
+ end
29
+
30
+ if @current_optgroup
31
+ @current_optgroup.options << question_option
32
+ else
33
+ @question.options << question_option
34
+ end
14
35
  end
15
36
  end
16
37
  end
@@ -7,10 +7,11 @@ module Quby::Compiler
7
7
  # sum(number_values(:v_1, :v_2))
8
8
  # end
9
9
  class ::SexpVariableBuilder
10
- attr_reader :calculation, :key
10
+ attr_reader :calculation, :key, :type
11
11
 
12
- def initialize(key, &block)
12
+ def initialize(key, type:, &block)
13
13
  @key = key
14
+ @type = type
14
15
  @calculation = instance_eval(&block)
15
16
  end
16
17
 
@@ -32,25 +33,39 @@ module Quby::Compiler
32
33
  end
33
34
  end
34
35
 
36
+ %i[eq not_eq gt gteq lt lteq].each do |op|
37
+ define_method(op) do |left:, right:|
38
+ Entities::SexpVariables::Comparer.new(op:, left: wrap_scalars(left), right: wrap_scalars(right))
39
+ end
40
+ end
41
+
42
+ %i[all some none].each do |op|
43
+ define_method(op) do |*values|
44
+ Entities::SexpVariables::BooleanReducer.new(op:, values:)
45
+ end
46
+ end
47
+
35
48
  def round(value)
36
49
  Entities::SexpVariables::NumberMethod.new(op: :round, value: value)
37
50
  end
38
51
 
39
52
  def build
40
- Entities::SexpVariable.new(key:, calculation:)
53
+ Entities::SexpVariable.new(key:, calculation:, type:)
41
54
  end
42
55
 
43
56
  private
44
57
 
45
58
  def wrap_and_flatten(values)
46
- values.flat_map { |value|
47
- case value
48
- when Numeric
49
- Entities::SexpVariables::Number.new(op: :number, value: value)
50
- else
51
- value
52
- end
53
- }
59
+ values.flat_map { wrap_scalars(_1)}
60
+ end
61
+
62
+ def wrap_scalars(value)
63
+ case value
64
+ when Numeric
65
+ Entities::SexpVariables::Number.new(op: :number, value: value)
66
+ else
67
+ value
68
+ end
54
69
  end
55
70
  end
56
71
  end
@@ -46,7 +46,7 @@ module Quby
46
46
  @question_hash[question.key] = question
47
47
  @input_keys.merge(new_input_keys)
48
48
  @answer_keys.merge(new_answer_keys)
49
- question.options.each do |option|
49
+ question.all_options.each do |option|
50
50
  @option_hash[option.input_key] = option
51
51
  end
52
52
  end
@@ -45,6 +45,7 @@ module Quby
45
45
 
46
46
  # Multiple-choice questions have options to choose from
47
47
  attr_accessor :options
48
+ attr_accessor :opt_groups
48
49
 
49
50
  # string question that is displayed after the title of this question.
50
51
  attr_accessor :title_question
@@ -246,10 +247,21 @@ module Quby
246
247
  answer_keys
247
248
  else
248
249
  # Some options don't have a key (inner_title), they are stripped
249
- options.map { |opt| opt.input_key }.compact
250
+ all_options.map { |opt| opt.input_key }.compact
250
251
  end
251
252
  end
252
253
 
254
+ def all_options
255
+ options.flat_map { |option|
256
+ case option
257
+ when Quby::Compiler::Entities::QuestionOptgroup
258
+ option.options
259
+ else
260
+ option
261
+ end
262
+ }
263
+ end
264
+
253
265
  def key_in_use?(k)
254
266
  claimed_keys.include?(k) ||
255
267
  options.any? { |option| option.key_in_use?(k) }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quby
4
+ module Compiler
5
+ module Entities
6
+ class QuestionOptgroup
7
+ include ActiveModel::Validations
8
+ include ::Quby::InspectExcept.new(:@question, :@questions)
9
+
10
+ attr_reader :key
11
+ attr_reader :label
12
+ attr_reader :options
13
+
14
+ def initialize(key, label:)
15
+ @key = key
16
+ @label = label
17
+ @options = []
18
+ end
19
+
20
+ def key_in_use?(key)
21
+ @options.any? { _1.key_in_use?(key) }
22
+ end
23
+
24
+ # We don't allow subquestions on select questions
25
+ def questions
26
+ []
27
+ end
28
+
29
+ def path_str
30
+ "#{question.path_str}:Optgroup:#{key}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,25 +2,53 @@ require 'quby/compiler/entities/sexp_variables'
2
2
 
3
3
  module Quby::Compiler::Entities
4
4
  class SexpVariable
5
- attr_reader :calculation, :key
5
+ attr_reader :calculation, :key, :type
6
6
 
7
- def initialize(key:, calculation:)
7
+ def initialize(key:, calculation:, type:)
8
8
  @key = key
9
+ @type = type
9
10
  @calculation = calculation
10
11
  end
11
12
 
12
13
  # Called by DefinitionValidator.
13
- def validate(questionnaire)
14
- case calculation
14
+ def validate(questionnaire, calculation: self.calculation)
15
+ validate_node(calculation, questionnaire:, type:)
16
+ end
17
+
18
+ private
19
+
20
+ def validate_node(node, questionnaire:, type:)
21
+ validate_type(node, type:)
22
+
23
+ case node
15
24
  when SexpVariables::NumberValue
16
- validate_question_exist(questionnaire, calculation)
17
- validate_value_is_number(questionnaire, calculation)
25
+ validate_question_exist(questionnaire, node)
26
+ validate_value_is_number(questionnaire, node)
18
27
  when SexpVariables::StringValue
19
- validate_question_exist(questionnaire, calculation)
20
- validate_value_is_string(questionnaire, calculation)
28
+ validate_question_exist(questionnaire, node)
29
+ validate_value_is_string(questionnaire, node)
30
+ when SexpVariables::NumberReducer
31
+ calculation.values.each do |value|
32
+ validate_node(value, questionnaire:, type: :number)
33
+ end
34
+ when SexpVariables::BooleanReducer
35
+ calculation.values.each do |value|
36
+ validate_node(value, questionnaire:, type: :boolean)
37
+ end
38
+ when SexpVariables::Comparer
39
+ validate_node(node.left, questionnaire:, type: :number)
40
+ validate_node(node.right, questionnaire:, type: :number)
21
41
  end
22
42
  end
23
43
 
44
+ def validate_type(node, type:)
45
+ return if type == :number && SexpVariables::NumericType === node
46
+ return if type == :boolean && SexpVariables::BooleanType === node
47
+ return if type == :string && SexpVariables::StringValue === node
48
+
49
+ fail "sexp_variable #{key} has invalid type #{node.class} for expected type #{type}."
50
+ end
51
+
24
52
  def validate_question_exist(questionnaire, sexp)
25
53
  return if questionnaire.question_hash.key?(sexp.key)
26
54
 
@@ -8,8 +8,11 @@ module Quby::Compiler::Entities
8
8
  class Number < Base; end
9
9
  class NumberReducer < Base; end
10
10
  class NumberMethod < Base; end
11
+ class BooleanReducer < Base; end
12
+ class Comparer < Base; end
11
13
 
12
14
  NumericType = Number | NumberReducer | NumberMethod | NumberValue
15
+ BooleanType = BooleanReducer | Comparer
13
16
 
14
17
  class NumberValue
15
18
  attribute :op, Quby::Types::Symbol.enum(:number_value)
@@ -32,6 +35,19 @@ module Quby::Compiler::Entities
32
35
  attribute :values, Quby::Types::Array.of(NumericType)
33
36
  end
34
37
 
38
+ # returning a Boolean
39
+ class BooleanReducer < Base
40
+ attribute :op, Quby::Types::Symbol.enum(:all, :some, :none)
41
+ attribute :values, Quby::Types::Array.of(Base)
42
+ end
43
+
44
+ # returning a Boolean
45
+ class Comparer < Base
46
+ attribute :op, Quby::Types::Symbol.enum(:eq, :not_eq, :gt, :gteq, :lt, :lteq)
47
+ attribute :left, NumericType
48
+ attribute :right, NumericType
49
+ end
50
+
35
51
  # returning a Number
36
52
  class NumberMethod
37
53
  attribute :op, Quby::Types::Symbol.enum(:round)
@@ -21,7 +21,7 @@ module Quby
21
21
 
22
22
  case question.type
23
23
  when :radio, :scale, :select
24
- question.options.each do |option|
24
+ question.all_options.each do |option|
25
25
  rules.concat rules_for_option(question, option, type: :equal)
26
26
  end
27
27
  when :check_box
@@ -5,6 +5,7 @@ require 'quby/compiler/entities/definition'
5
5
  require 'quby/compiler/entities/questionnaire'
6
6
  require 'quby/compiler/entities/version'
7
7
  require 'quby/compiler/entities/question_option'
8
+ require 'quby/compiler/entities/question_optgroup'
8
9
  require 'quby/compiler/entities/item'
9
10
  require 'quby/compiler/entities/score_calculation'
10
11
  require 'quby/compiler/entities/anonymous_conditions'
@@ -183,7 +183,7 @@ module Quby
183
183
  # Also adds the title question under the last option.
184
184
  # TODO old dsl put it there, remove after quby gem uses title_question
185
185
  def options_as_json(question)
186
- question.options.map { |option| option_as_json(option) }.tap do |options_json|
186
+ question.all_options.map { |option| option_as_json(option) }.tap do |options_json|
187
187
  if question.title_question
188
188
  options_json.last[:questions] << question_as_json(question.title_question)
189
189
  end
@@ -163,14 +163,17 @@ module Quby
163
163
  end
164
164
 
165
165
  def scale_question(question)
166
- radio_question(question)
166
+ {
167
+ **radio_question(question),
168
+ presentation: :horizontal
169
+ }
167
170
  end
168
171
 
169
172
  def select_question(question)
170
173
  {
171
174
  **base_question(question),
172
175
  children: children(question),
173
- placeholder: question.options.find { _1.placeholder }&.description,
176
+ placeholder: question.all_options.find { _1.placeholder }&.description,
174
177
  }.compact
175
178
  end
176
179
 
@@ -242,7 +245,12 @@ module Quby
242
245
 
243
246
  def children(question)
244
247
  question.options.map.with_index { |child, idx|
245
- if child.inner_title
248
+ if child.is_a?(Quby::Compiler::Entities::QuestionOptgroup)
249
+ { type: 'optgroup',
250
+ key: child.key,
251
+ options: child.options.map { option_as_json(_1) }
252
+ }
253
+ elsif child.inner_title
246
254
  inner_title_as_json(child, idx)
247
255
  elsif child.placeholder
248
256
  nil # placeholder attr on question.
@@ -271,7 +279,7 @@ module Quby
271
279
  value: option.question.type != :check_box && option.value,
272
280
  label:,
273
281
  description:,
274
- questions: option.question.type != :select && option.questions.map{ question(_1) },
282
+ questions: option.question.type != :select ? option.questions.map{ question(_1) } : nil,
275
283
  hidden: option.hidden.presence,
276
284
  viewId: option.view_id
277
285
  }.compact
@@ -239,7 +239,7 @@ module Quby
239
239
  def options_for(question)
240
240
  return nil if question.options.empty?
241
241
 
242
- question.options.reject { |option| option.inner_title || option.placeholder }
242
+ question.all_options.reject { |option| option.inner_title || option.placeholder }
243
243
  .map { |option|
244
244
  {
245
245
  key: option.key,
@@ -1,5 +1,5 @@
1
1
  module Quby
2
2
  module Compiler
3
- VERSION = "0.5.28"
3
+ VERSION = "0.5.30"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quby-compiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.28
4
+ version: 0.5.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marten Veldthuis
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-08-27 00:00:00.000000000 Z
10
+ date: 2025-09-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activemodel
@@ -151,7 +151,6 @@ files:
151
151
  - lib/quby/compiler/dsl/charting/radar_chart_builder.rb
152
152
  - lib/quby/compiler/dsl/helpers.rb
153
153
  - lib/quby/compiler/dsl/info_block_builder.rb
154
- - lib/quby/compiler/dsl/merge(values(:v_1, :v_2), values(v_4).rb
155
154
  - lib/quby/compiler/dsl/panel_builder.rb
156
155
  - lib/quby/compiler/dsl/question_builder.rb
157
156
  - lib/quby/compiler/dsl/questionnaire_builder.rb
@@ -188,6 +187,7 @@ files:
188
187
  - lib/quby/compiler/entities/outcome_table.rb
189
188
  - lib/quby/compiler/entities/panel.rb
190
189
  - lib/quby/compiler/entities/question.rb
190
+ - lib/quby/compiler/entities/question_optgroup.rb
191
191
  - lib/quby/compiler/entities/question_option.rb
192
192
  - lib/quby/compiler/entities/questionnaire.rb
193
193
  - lib/quby/compiler/entities/questions/checkbox_question.rb
@@ -1,16 +0,0 @@
1
- merge(values(:v_1, :v_2), values(v_4).recode(1 => 3)).sum
2
-
3
-
4
-
5
- divide(value(:v_2), multiply(value(:v_1), value(:v_1)))
6
-
7
- w / l * l
8
-
9
-
10
- sexp_variable :key1 do
11
- foo = values(:v_1, :v_2)
12
- bar = recode(foo, 1 => 3)
13
- bla = merge(foo, bar)
14
- sum(bla)
15
-
16
- end