formalist 0.1.0

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +96 -0
  4. data/LICENSE.md +9 -0
  5. data/README.md +27 -0
  6. data/Rakefile +13 -0
  7. data/lib/formalist/definition_compiler.rb +61 -0
  8. data/lib/formalist/display_adapters/default.rb +9 -0
  9. data/lib/formalist/display_adapters/radio.rb +19 -0
  10. data/lib/formalist/display_adapters/select.rb +19 -0
  11. data/lib/formalist/display_adapters.rb +12 -0
  12. data/lib/formalist/form/definition/attr.rb +20 -0
  13. data/lib/formalist/form/definition/component.rb +32 -0
  14. data/lib/formalist/form/definition/field.rb +43 -0
  15. data/lib/formalist/form/definition/group.rb +31 -0
  16. data/lib/formalist/form/definition/many.rb +41 -0
  17. data/lib/formalist/form/definition/section.rb +23 -0
  18. data/lib/formalist/form/definition.rb +37 -0
  19. data/lib/formalist/form/definition_context.rb +15 -0
  20. data/lib/formalist/form/result/attr.rb +82 -0
  21. data/lib/formalist/form/result/component.rb +51 -0
  22. data/lib/formalist/form/result/field.rb +77 -0
  23. data/lib/formalist/form/result/group.rb +51 -0
  24. data/lib/formalist/form/result/many.rb +127 -0
  25. data/lib/formalist/form/result/section.rb +54 -0
  26. data/lib/formalist/form/result.rb +15 -0
  27. data/lib/formalist/form.rb +44 -0
  28. data/lib/formalist/output_compiler.rb +65 -0
  29. data/lib/formalist/validation/collection_rules_compiler.rb +77 -0
  30. data/lib/formalist/validation/predicate_list_compiler.rb +73 -0
  31. data/lib/formalist/validation/value_rules_compiler.rb +92 -0
  32. data/lib/formalist/version.rb +3 -0
  33. data/lib/formalist.rb +7 -0
  34. data/spec/examples.txt +8 -0
  35. data/spec/integration/display_adapters_spec.rb +49 -0
  36. data/spec/integration/form_spec.rb +22 -0
  37. data/spec/integration/new_spec.rb +27 -0
  38. data/spec/integration/validation_spec.rb +83 -0
  39. data/spec/spec_helper.rb +102 -0
  40. data/spec/unit/output_compiler_spec.rb +51 -0
  41. metadata +252 -0
@@ -0,0 +1,77 @@
1
+ require "formalist/validation/value_rules_compiler"
2
+ require "formalist/validation/predicate_list_compiler"
3
+
4
+ module Formalist
5
+ class Form
6
+ class Result
7
+ class Field
8
+ attr_reader :definition, :input, :rules, :predicates, :errors
9
+
10
+ def initialize(definition, input, rules, errors)
11
+ rules_compiler = Validation::ValueRulesCompiler.new(definition.name)
12
+ predicates_compiler = Validation::PredicateListCompiler.new
13
+
14
+ @definition = definition
15
+ @input = input[definition.name]
16
+ @rules = rules_compiler.(rules)
17
+ @predicates = predicates_compiler.(@rules)
18
+ @errors = errors[definition.name].to_a[0] || []
19
+ end
20
+
21
+ # Converts the field into an array format for including in a form's
22
+ # abstract syntax tree.
23
+ #
24
+ # The array takes the following format:
25
+ #
26
+ # ```
27
+ # [:field, [params]]
28
+ # ```
29
+ #
30
+ # With the following parameters:
31
+ #
32
+ # 1. Field name
33
+ # 1. Field type
34
+ # 1. Display variant name
35
+ # 1. Input data
36
+ # 1. Validation rules (if any)
37
+ # 1. Validation error messages (if any)
38
+ # 1. Field configuration
39
+ #
40
+ # @example "email" field
41
+ # field.to_ary # =>
42
+ # # [:field, [
43
+ # # :email,
44
+ # # "string",
45
+ # # "default",
46
+ # # "invalid email value",
47
+ # # [
48
+ # # [:and, [
49
+ # # [:predicate, [:filled?, []]],
50
+ # # [:predicate, [:format?, [/\s+@\s+\.\s+/]]]
51
+ # # ]]
52
+ # # ],
53
+ # # ["email is in invalid format"],
54
+ # # [
55
+ # # [:some_config_name, :some_config_value]
56
+ # # ]
57
+ # # ]]
58
+ #
59
+ # @return [Array] the field as an array.
60
+ def to_ary
61
+ # errors looks like this
62
+ # {:field_name => [["pages is missing", "another error message"], nil]}
63
+
64
+ [:field, [
65
+ definition.name,
66
+ definition.type,
67
+ definition.display_variant,
68
+ Dry::Data[definition.type].(input),
69
+ predicates,
70
+ errors,
71
+ definition.config.to_a,
72
+ ]]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,51 @@
1
+ module Formalist
2
+ class Form
3
+ class Result
4
+ class Group
5
+ attr_reader :definition, :input, :errors
6
+ attr_reader :children
7
+
8
+ def initialize(definition, input, rules, errors)
9
+ @definition = definition
10
+ @input = input
11
+ @rules = rules
12
+ @errors = errors
13
+ @children = definition.children.map { |el| el.(input, rules, errors) }
14
+ end
15
+
16
+ # Converts the group into an array format for including in a form's
17
+ # abstract syntax tree.
18
+ #
19
+ # The array takes the following format:
20
+ #
21
+ # ```
22
+ # [:group, [params]]
23
+ # ```
24
+ #
25
+ # With the following parameters:
26
+ #
27
+ # 1. Group configuration
28
+ # 1. Child form elements
29
+ #
30
+ # @example
31
+ # group.to_ary # =>
32
+ # # [:group, [
33
+ # # [
34
+ # # [:some_config_name, :some_config_value]
35
+ # # ],
36
+ # # [
37
+ # # ...child elements...
38
+ # # ]
39
+ # # ]]
40
+ #
41
+ # @return [Array] the group as an array.
42
+ def to_ary
43
+ [:group, [
44
+ definition.config.to_a,
45
+ children.map(&:to_ary),
46
+ ]]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,127 @@
1
+ require "formalist/validation/collection_rules_compiler"
2
+ require "formalist/validation/value_rules_compiler"
3
+ require "formalist/validation/predicate_list_compiler"
4
+
5
+ module Formalist
6
+ class Form
7
+ class Result
8
+ class Many
9
+ attr_reader :definition, :input, :value_rules, :value_predicates, :collection_rules, :errors
10
+ attr_reader :child_template, :children
11
+
12
+ def initialize(definition, input, rules, errors)
13
+ value_rules_compiler = Validation::ValueRulesCompiler.new(definition.name)
14
+ value_predicates_compiler = Validation::PredicateListCompiler.new
15
+ collection_rules_compiler = Validation::CollectionRulesCompiler.new(definition.name)
16
+
17
+ @definition = definition
18
+ @input = input.fetch(definition.name, [])
19
+ @value_rules = value_rules_compiler.(rules)
20
+ @value_predicates = value_predicates_compiler.(@value_rules)
21
+ @collection_rules = collection_rules_compiler.(rules)
22
+ @errors = errors.fetch(definition.name, [])[0] || []
23
+ @child_template = build_child_template
24
+ @children = build_children
25
+ end
26
+
27
+ # Converts a collection of "many" repeating elements into an array
28
+ # format for including in a form's abstract syntax tree.
29
+ #
30
+ # The array takes the following format:
31
+ #
32
+ # ```
33
+ # [:many, [params]]
34
+ # ```
35
+ #
36
+ # With the following parameters:
37
+ #
38
+ # 1. Collection array name
39
+ # 1. Collection validation rules (if any)
40
+ # 1. Collection error messages (if any)
41
+ # 1. Collection configuration
42
+ # 1. Child element "template" (i.e. the form elements comprising a
43
+ # single entry in the collection of "many" elements, without any
44
+ # user data associated)
45
+ # 1. Child elements, one for each of the entries in the input data (or
46
+ # none, if there is no or empty input data)
47
+ #
48
+ # @example "locations" collection
49
+ # many.to_ary # =>
50
+ # # [:many, [
51
+ # # :locations,
52
+ # # [[:predicate, [:min_size?, [3]]]],
53
+ # # ["locations size cannot be less than 3"],
54
+ # # [
55
+ # # [:allow_create, true],
56
+ # # [:allow_update, true],
57
+ # # [:allow_destroy, true],
58
+ # # [:allow_reorder, true]
59
+ # # ],
60
+ # # [
61
+ # # [:field, [:name, "string", "default", nil, [], [], []]],
62
+ # # [:field, [:address, "string", "default", nil, [], [], []]]
63
+ # # [
64
+ # # [
65
+ # # [:field, [:name, "string", "default", "Icelab Canberra", [], [], []]],
66
+ # # [:field, [:address, "string", "default", "Canberra, ACT, Australia", [], [], []]]
67
+ # # ],
68
+ # # [
69
+ # # [:field, [:name, "string", "default", "Icelab Melbourne", [], [], []]],
70
+ # # [:field, [:address, "string", "default", "Melbourne, VIC, Australia", [], [], []]]
71
+ # # ]
72
+ # # ]
73
+ # # ]]
74
+ #
75
+ # @return [Array] the collection as an array.
76
+ def to_ary
77
+ local_errors = errors.select { |e| e.is_a?(String) }
78
+
79
+ [:many, [
80
+ definition.name,
81
+ value_predicates,
82
+ local_errors,
83
+ definition.config.to_a,
84
+ child_template.map(&:to_ary),
85
+ children.map { |el_list| el_list.map(&:to_ary) },
86
+ ]]
87
+ end
88
+
89
+ private
90
+
91
+ def build_child_template
92
+ template_input = {}
93
+ template_errors = {}
94
+
95
+ definition.children.map { |el| el.(template_input, collection_rules, template_errors)}
96
+ end
97
+
98
+ def build_children
99
+ # child errors looks like this:
100
+ # {:links=>
101
+ # [[{:links=>
102
+ # [[{:url=>[["url must be filled"], ""]}],
103
+ # {:name=>"personal", :url=>""}]}],
104
+ # [{:name=>"company", :url=>"http://icelab.com.au"},
105
+ # {:name=>"personal", :url=>""}]]}
106
+ #
107
+ # or local errors:
108
+ # {:links=>[["links is missing"], nil]}
109
+
110
+ child_errors = errors[0].is_a?(Hash) ? errors : {}
111
+
112
+ input.map { |child_input|
113
+ local_child_errors = child_errors.select { |e|
114
+ e.is_a?(Hash)
115
+ }.map { |e|
116
+ e[definition.name]
117
+ }.detect { |e|
118
+ e[1] == child_input
119
+ }.to_a.dig(0, 0) || {}
120
+
121
+ definition.children.map { |el| el.(child_input, collection_rules, local_child_errors) }
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,54 @@
1
+ module Formalist
2
+ class Form
3
+ class Result
4
+ class Section
5
+ attr_reader :definition, :input, :rules, :errors
6
+ attr_reader :children
7
+
8
+ def initialize(definition, input, rules, errors)
9
+ @definition = definition
10
+ @input = input
11
+ @rules = rules
12
+ @errors = errors
13
+ @children = definition.children.map { |el| el.(input, rules, errors) }
14
+ end
15
+
16
+ # Converts the section into an array format for including in a form's
17
+ # abstract syntax tree.
18
+ #
19
+ # The array takes the following format:
20
+ #
21
+ # ```
22
+ # [:section, [params]]
23
+ # ```
24
+ #
25
+ # With the following parameters:
26
+ #
27
+ # 1. Section name
28
+ # 1. Section configuration
29
+ # 1. Child form elements
30
+ #
31
+ # @example "content" section
32
+ # section.to_ary # =>
33
+ # # [:section, [
34
+ # # :content,
35
+ # # [
36
+ # # [:some_config_name, :some_config_value]
37
+ # # ],
38
+ # # [
39
+ # # ...child elements...
40
+ # # ]
41
+ # # ]]
42
+ #
43
+ # @return [Array] the section as an array.
44
+ def to_ary
45
+ [:section, [
46
+ definition.name,
47
+ definition.config.to_a,
48
+ children.map(&:to_ary),
49
+ ]]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ module Formalist
2
+ class Form
3
+ class Result
4
+ attr_reader :elements
5
+
6
+ def initialize(elements)
7
+ @elements = elements
8
+ end
9
+
10
+ def to_ary
11
+ elements.map(&:to_ary)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ require "dry-configurable"
2
+ require "formalist/definition_compiler"
3
+ require "formalist/display_adapters"
4
+ require "formalist/form/definition"
5
+ require "formalist/form/result"
6
+
7
+ module Formalist
8
+ class Form
9
+ extend Dry::Configurable
10
+ extend Definition
11
+
12
+ setting :display_adapters, DisplayAdapters
13
+
14
+ def self.display_adapters
15
+ config.display_adapters
16
+ end
17
+
18
+ # @api private
19
+ def self.elements
20
+ @__elements__ ||= []
21
+ end
22
+
23
+ # @api private
24
+ attr_reader :elements
25
+
26
+ # @api private
27
+ attr_reader :schema
28
+
29
+ def initialize(schema: nil)
30
+ definition_compiler = DefinitionCompiler.new(self.class.display_adapters)
31
+ @elements = definition_compiler.call(self.class.elements)
32
+ @schema = schema
33
+ end
34
+
35
+ def call(input = {}, options = {})
36
+ validate = options.fetch(:validate, true)
37
+
38
+ rules = schema ? schema.rules.map(&:to_ary) : []
39
+ errors = validate && schema ? schema.(input).messages : {}
40
+
41
+ Result.new(elements.map { |el| el.(input, rules, errors) })
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,65 @@
1
+ require "dry-data"
2
+
3
+ module Formalist
4
+ class OutputCompiler
5
+ FORM_TYPES = %w[
6
+ bool
7
+ date
8
+ date_time
9
+ decimal
10
+ float
11
+ int
12
+ time
13
+ ].freeze
14
+
15
+ def call(ast)
16
+ ast.map { |node| visit(node) }.inject(:merge)
17
+ end
18
+
19
+ private
20
+
21
+ def visit(node)
22
+ send(:"visit_#{node[0]}", node[1])
23
+ end
24
+
25
+ def visit_attr(data)
26
+ name, predicates, errors, children = data
27
+
28
+ {name => children.map { |node| visit(node) }.inject(:merge) }
29
+ end
30
+
31
+ def visit_field(data)
32
+ name, type, display_variant, value, predicates, errors = data
33
+
34
+ {name => coerce(value, type: type)}
35
+ end
36
+
37
+ def visit_group(data)
38
+ config, children = data
39
+
40
+ children.map { |node| visit(node) }.inject(:merge)
41
+ end
42
+
43
+ def visit_many(data)
44
+ name, predicates, errors, config, template, children = data
45
+
46
+ {name => children.map { |item| item.map { |node| visit(node) }.inject(:merge) }}
47
+ end
48
+
49
+ def visit_section(data)
50
+ name, config, children = data
51
+
52
+ children.map { |node| visit(node) }.inject(:merge)
53
+ end
54
+
55
+ private
56
+
57
+ def coerce(value, type:)
58
+ if FORM_TYPES.include?(type)
59
+ Dry::Data["form.#{type}"].(value)
60
+ else
61
+ Dry::Data[type].(value)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ module Formalist
2
+ module Validation
3
+ class CollectionRulesCompiler
4
+ attr_reader :target_name
5
+
6
+ def initialize(target_name)
7
+ @target_name = target_name
8
+ end
9
+
10
+ def call(ast)
11
+ ast.map { |node| visit(node) }.reduce([], :concat).each_slice(2).to_a
12
+ end
13
+
14
+ private
15
+
16
+ def visit(node)
17
+ name, nodes = node
18
+ send(:"visit_#{name}", nodes)
19
+ end
20
+
21
+ def visit_set(node)
22
+ name, rules = node
23
+ return [] unless name == target_name
24
+
25
+ rules.flatten(1)
26
+ end
27
+
28
+ def visit_each(node)
29
+ name, rule = node
30
+ return [] unless name == target_name
31
+
32
+ visit(rule)
33
+ end
34
+
35
+ def visit_predicate(node)
36
+ name, args = node
37
+ [:predicate, node]
38
+ end
39
+
40
+ def visit_and(node)
41
+ left, right = node
42
+ flatten_logical_operation(:and, [visit(left), visit(right)])
43
+ end
44
+
45
+ def visit_or(node)
46
+ left, right = node
47
+ flatten_logical_operation(:or, [visit(left), visit(right)])
48
+ end
49
+
50
+ def visit_xor(node)
51
+ left, right = node
52
+ flatten_logical_operation(:xor, [visit(left), visit(right)])
53
+ end
54
+
55
+ def visit_implication(node)
56
+ left, right = node
57
+ flatten_logical_operation(:implication, [visit(left), visit(right)])
58
+ end
59
+
60
+ def method_missing(name, *args)
61
+ []
62
+ end
63
+
64
+ def flatten_logical_operation(name, contents)
65
+ contents = contents.select(&:any?)
66
+
67
+ if contents.length == 0
68
+ []
69
+ elsif contents.length == 1
70
+ contents.first
71
+ else
72
+ [name, contents]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,73 @@
1
+ module Formalist
2
+ module Validation
3
+ class PredicateListCompiler
4
+ IGNORED_PREDICATES = [:key?].freeze
5
+
6
+ def call(ast)
7
+ ast.map { |node| visit(node) }.reduce([], :concat).each_slice(2).to_a
8
+ end
9
+
10
+ private
11
+
12
+ def visit(node)
13
+ name, nodes = node
14
+ send(:"visit_#{name}", nodes)
15
+ end
16
+
17
+ def visit_key(node)
18
+ name, predicate = node
19
+
20
+ visit(predicate)
21
+ end
22
+
23
+ def visit_val(node)
24
+ name, predicate = node
25
+
26
+ visit(predicate)
27
+ end
28
+
29
+ def visit_predicate(node)
30
+ name, args = node
31
+ return [] if IGNORED_PREDICATES.include?(name)
32
+
33
+ [:predicate, node]
34
+ end
35
+
36
+ def visit_and(node)
37
+ left, right = node
38
+ flatten_logical_operation(:and, [visit(left), visit(right)])
39
+ end
40
+
41
+ def visit_or(node)
42
+ left, right = node
43
+ flatten_logical_operation(:or, [visit(left), visit(right)])
44
+ end
45
+
46
+ def visit_xor(node)
47
+ left, right = node
48
+ flatten_logical_operation(:xor, [visit(left), visit(right)])
49
+ end
50
+
51
+ def visit_implication(node)
52
+ left, right = node
53
+ flatten_logical_operation(:implication, [visit(left), visit(right)])
54
+ end
55
+
56
+ def method_missing(name, *args)
57
+ []
58
+ end
59
+
60
+ def flatten_logical_operation(name, contents)
61
+ contents = contents.select(&:any?)
62
+
63
+ if contents.length == 0
64
+ []
65
+ elsif contents.length == 1
66
+ contents.first
67
+ else
68
+ [name, contents]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,92 @@
1
+ module Formalist
2
+ module Validation
3
+ class ValueRulesCompiler
4
+ attr_reader :target_name
5
+
6
+ def initialize(target_name)
7
+ @target_name = target_name
8
+ end
9
+
10
+ def call(ast)
11
+ ast.map { |node| visit(node) }.reduce([], :concat).each_slice(2).to_a
12
+ end
13
+
14
+ private
15
+
16
+ def visit(node)
17
+ name, nodes = node
18
+ send(:"visit_#{name}", nodes)
19
+ end
20
+
21
+ def visit_key(node)
22
+ # We can ignore "key" checks - we'll only pick up rules for keys we
23
+ # know will exist, since they're attached to fields.
24
+ []
25
+ end
26
+
27
+ def visit_val(node)
28
+ name, predicate = node
29
+ return [] unless name == target_name
30
+
31
+ # Skip the "val" prefix
32
+
33
+ [:val, [name, visit(predicate)]]
34
+ end
35
+
36
+ # def visit_set(node)
37
+ # name, rules = node
38
+ # return [] unless name == target_name
39
+
40
+ # rules.flatten(1)
41
+ # end
42
+
43
+ # def visit_each(node)
44
+ # name, rule = node
45
+ # return [] unless name == target_name
46
+
47
+ # visit(rule)
48
+ # end
49
+
50
+ def visit_predicate(node)
51
+ name, args = node
52
+ [:predicate, node]
53
+ end
54
+
55
+ def visit_and(node)
56
+ left, right = node
57
+ flatten_logical_operation(:and, [visit(left), visit(right)])
58
+ end
59
+
60
+ def visit_or(node)
61
+ left, right = node
62
+ flatten_logical_operation(:or, [visit(left), visit(right)])
63
+ end
64
+
65
+ def visit_xor(node)
66
+ left, right = node
67
+ flatten_logical_operation(:xor, [visit(left), visit(right)])
68
+ end
69
+
70
+ def visit_implication(node)
71
+ left, right = node
72
+ flatten_logical_operation(:implication, [visit(left), visit(right)])
73
+ end
74
+
75
+ def method_missing(name, *args)
76
+ []
77
+ end
78
+
79
+ def flatten_logical_operation(name, contents)
80
+ contents = contents.select(&:any?)
81
+
82
+ if contents.length == 0
83
+ []
84
+ elsif contents.length == 1
85
+ contents.first
86
+ else
87
+ [name, contents]
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module Formalist
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/formalist.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Formalist
2
+ DEFAULT_DISPLAY_ADAPTER = "default".freeze
3
+ end
4
+
5
+ require "dry-validation"
6
+ require "formalist/form"
7
+ require "formalist/output_compiler"
data/spec/examples.txt ADDED
@@ -0,0 +1,8 @@
1
+ example_id | status | run_time |
2
+ ------------------------------------------------ | ------ | --------------- |
3
+ ./spec/integration/display_adapters_spec.rb[1:1] | passed | 0.00016 seconds |
4
+ ./spec/integration/display_adapters_spec.rb[1:2] | passed | 0.00024 seconds |
5
+ ./spec/integration/form_spec.rb[1:1] | passed | 0.00021 seconds |
6
+ ./spec/integration/new_spec.rb[1:1] | passed | 0.00063 seconds |
7
+ ./spec/integration/validation_spec.rb[1:1] | passed | 0.00435 seconds |
8
+ ./spec/unit/output_compiler_spec.rb[1:1] | passed | 0.00049 seconds |