formalist 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 |