formalist 0.3.0 → 0.4.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 (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +7 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +21 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +62 -0
  7. data/Gemfile +1 -2
  8. data/bin/console +13 -0
  9. data/formalist.gemspec +33 -0
  10. data/lib/formalist/definition.rb +65 -0
  11. data/lib/formalist/element/class_interface.rb +7 -59
  12. data/lib/formalist/element.rb +37 -19
  13. data/lib/formalist/elements/attr.rb +8 -20
  14. data/lib/formalist/elements/compound_field.rb +5 -4
  15. data/lib/formalist/elements/field.rb +5 -12
  16. data/lib/formalist/elements/group.rb +6 -5
  17. data/lib/formalist/elements/many.rb +28 -29
  18. data/lib/formalist/elements/section.rb +6 -10
  19. data/lib/formalist/elements/standard/multi_upload_field.rb +8 -0
  20. data/lib/formalist/elements/standard/rich_text_area.rb +40 -0
  21. data/lib/formalist/elements/standard/search_multi_selection_field.rb +20 -0
  22. data/lib/formalist/elements/standard/search_selection_field.rb +20 -0
  23. data/lib/formalist/elements/standard/tags_field.rb +16 -0
  24. data/lib/formalist/elements/standard/upload_field.rb +8 -0
  25. data/lib/formalist/elements/standard.rb +4 -0
  26. data/lib/formalist/form/validity_check.rb +54 -0
  27. data/lib/formalist/form.rb +49 -17
  28. data/lib/formalist/rich_text/embedded_form_compiler.rb +86 -0
  29. data/lib/formalist/rich_text/embedded_forms_container/mixin.rb +42 -0
  30. data/lib/formalist/rich_text/embedded_forms_container/registration.rb +30 -0
  31. data/lib/formalist/rich_text/embedded_forms_container.rb +9 -0
  32. data/lib/formalist/rich_text/rendering/embedded_form_renderer.rb +25 -0
  33. data/lib/formalist/rich_text/rendering/html_compiler.rb +100 -0
  34. data/lib/formalist/rich_text/rendering/html_renderer.rb +186 -0
  35. data/lib/formalist/rich_text/validity_check.rb +48 -0
  36. data/lib/formalist/types.rb +8 -7
  37. data/lib/formalist/version.rb +1 -1
  38. metadata +54 -31
  39. data/Gemfile.lock +0 -105
  40. data/lib/formalist/element/definition.rb +0 -55
  41. data/lib/formalist/element/permitted_children.rb +0 -46
  42. data/lib/formalist/form/definition_context.rb +0 -69
  43. data/lib/formalist/form/result.rb +0 -24
  44. data/spec/examples.txt +0 -8
  45. data/spec/integration/dependency_injection_spec.rb +0 -54
  46. data/spec/integration/form_spec.rb +0 -104
  47. data/spec/spec_helper.rb +0 -109
  48. data/spec/support/constants.rb +0 -11
  49. data/spec/unit/elements/standard/check_box_spec.rb +0 -33
@@ -0,0 +1,40 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+ require "formalist/rich_text/embedded_form_compiler"
5
+
6
+ module Formalist
7
+ class Elements
8
+ class RichTextArea < Field
9
+ attribute :box_size, Types::String.enum("single", "small", "normal", "large", "xlarge"), default: "normal"
10
+ attribute :inline_formatters, Types::Array
11
+ attribute :block_formatters, Types::Array
12
+ attribute :embeddable_forms, Types::Dependency.constrained(respond_to: :to_h)
13
+
14
+ # FIXME: it would be tidier to have a reader method for each attribute
15
+ def attributes
16
+ super.merge(embeddable_forms: embeddable_forms_config)
17
+ end
18
+
19
+ def input
20
+ input_compiler.(@input)
21
+ end
22
+
23
+ private
24
+
25
+ # Replace the form objects with their AST
26
+ def embeddable_forms_config
27
+ @attributes[:embeddable_forms].to_h.map { |key, attrs|
28
+ [key, attrs.merge(form: attrs[:form].to_ast)]
29
+ }.to_h
30
+ end
31
+
32
+ # TODO: make compiler configurable somehow?
33
+ def input_compiler
34
+ RichText::EmbeddedFormCompiler.new(@attributes[:embeddable_forms])
35
+ end
36
+ end
37
+
38
+ register :rich_text_area, RichTextArea
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class SearchMultiSelectionField < Field
8
+ attribute :selector_label, Types::String
9
+ attribute :render_option_as, Types::String
10
+ attribute :render_selection_as, Types::String
11
+ attribute :search_url, Types::String
12
+ attribute :search_per_page, Types::Int
13
+ attribute :search_params, Types::Hash
14
+ attribute :search_threshold, Types::Int
15
+ attribute :selections, Types::Array
16
+ end
17
+
18
+ register :search_multi_selection_field, SearchMultiSelectionField
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class SearchSelectionField < Field
8
+ attribute :selector_label, Types::String
9
+ attribute :render_option_as, Types::String
10
+ attribute :render_selection_as, Types::String
11
+ attribute :search_url, Types::String
12
+ attribute :search_per_page, Types::Int
13
+ attribute :search_params, Types::Hash
14
+ attribute :search_threshold, Types::Int
15
+ attribute :selection, Types::Hash
16
+ end
17
+
18
+ register :search_selection_field, SearchSelectionField
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class TagsField < Field
8
+ attribute :search_url, Types::String
9
+ attribute :search_per_page, Types::Int
10
+ attribute :search_params, Types::Hash
11
+ attribute :search_threshold, Types::Int
12
+ end
13
+
14
+ register :tags_field, TagsField
15
+ end
16
+ end
@@ -6,6 +6,14 @@ module Formalist
6
6
  class Elements
7
7
  class UploadField < Field
8
8
  attribute :presign_url, Types::String
9
+ attribute :presign_options, Types::Hash
10
+ attribute :render_uploaded_as, Types::String
11
+ attribute :upload_prompt, Types::String
12
+ attribute :upload_action_label, Types::String
13
+ attribute :max_file_size, Types::String
14
+ attribute :max_file_size_message, Types::String
15
+ attribute :permitted_file_type_message, Types::String
16
+ attribute :permitted_file_type_regex, Types::String
9
17
  end
10
18
 
11
19
  register :upload_field, UploadField
@@ -6,8 +6,12 @@ require "formalist/elements/standard/multi_selection_field"
6
6
  require "formalist/elements/standard/multi_upload_field"
7
7
  require "formalist/elements/standard/number_field"
8
8
  require "formalist/elements/standard/radio_buttons"
9
+ require "formalist/elements/standard/rich_text_area"
10
+ require "formalist/elements/standard/search_selection_field"
11
+ require "formalist/elements/standard/search_multi_selection_field"
9
12
  require "formalist/elements/standard/select_box"
10
13
  require "formalist/elements/standard/selection_field"
14
+ require "formalist/elements/standard/tags_field"
11
15
  require "formalist/elements/standard/text_area"
12
16
  require "formalist/elements/standard/text_field"
13
17
  require "formalist/elements/standard/upload_field"
@@ -0,0 +1,54 @@
1
+ module Formalist
2
+ class Form
3
+ class ValidityCheck
4
+ def call(form_ast)
5
+ form_ast.map { |node| visit(node) }.all?
6
+ end
7
+ alias_method :[], :call
8
+
9
+ private
10
+
11
+ def visit(node)
12
+ name, nodes = node
13
+
14
+ send(:"visit_#{name}", nodes)
15
+ end
16
+
17
+ def visit_attr(node)
18
+ _name, _type, errors, _attributes, children = node
19
+
20
+ errors.empty? && children.map { |child| visit(child) }.all?
21
+ end
22
+
23
+ def visit_compound_field(node)
24
+ _type, _attributes, children = node
25
+
26
+ children.map { |child| visit(child) }.all?
27
+ end
28
+
29
+ def visit_field(node)
30
+ _name, _type, _input, errors, _attributes = node
31
+
32
+ errors.empty?
33
+ end
34
+
35
+ def visit_group(node)
36
+ _type, _attributes, children = node
37
+
38
+ children.map { |child| visit(child) }.all?
39
+ end
40
+
41
+ def visit_many(node)
42
+ _name, _type, errors, _attributes, _child_template, children = node
43
+
44
+ errors.empty? && children.map { |child| visit(child) }.all?
45
+ end
46
+
47
+ def visit_section(node)
48
+ _name, _type, _attributes, children = node
49
+
50
+ children.map { |child| visit(child) }.all?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,32 +1,64 @@
1
- require "dry-configurable"
1
+ require "dry/configurable"
2
+ require "dry/core/constants"
2
3
  require "formalist/elements"
3
- require "formalist/form/definition_context"
4
- require "formalist/element/permitted_children"
5
- require "formalist/form/result"
4
+ require "formalist/definition"
6
5
 
7
6
  module Formalist
8
7
  class Form
9
8
  extend Dry::Configurable
9
+ include Dry::Core::Constants
10
10
 
11
11
  setting :elements_container, Elements
12
12
 
13
- # @api private
14
- def self.elements
15
- @elements ||= []
13
+ class << self
14
+ attr_reader :definition
15
+
16
+ def define(&block)
17
+ @definition = block
18
+ end
19
+ end
20
+
21
+ attr_reader :elements
22
+ attr_reader :input
23
+ attr_reader :errors
24
+ attr_reader :dependencies
25
+
26
+ def initialize(elements: Undefined, input: {}, errors: {}, **dependencies)
27
+ @input = input
28
+ @errors = errors
29
+
30
+ @elements =
31
+ if elements == Undefined
32
+ Definition.new(self, self.class.config, &self.class.definition).elements
33
+ else
34
+ elements
35
+ end
36
+
37
+ @dependencies = dependencies
38
+ end
39
+
40
+ def fill(input: {}, errors: {})
41
+ return self if input == @input && errors = @errors
42
+
43
+ self.class.new(
44
+ elements: @elements.map { |element| element.fill(input: input, errors: errors) },
45
+ input: input,
46
+ errors: errors,
47
+ **@dependencies,
48
+ )
16
49
  end
17
50
 
18
- # @api public
19
- def self.define(&block)
20
- @elements = DefinitionContext.new(
21
- container: config.elements_container,
22
- permissions: Element::PermittedChildren.all
23
- ).call(&block).elements
51
+ def with(**new_dependencies)
52
+ self.class.new(
53
+ elements: @elements,
54
+ input: @input,
55
+ errors: @errors,
56
+ **@dependencies.merge(new_dependencies)
57
+ )
24
58
  end
25
59
 
26
- # @api public
27
- def build(input = {}, messages = {})
28
- elements = self.class.elements.map { |el| el.resolve(self) }
29
- Result.new(input, messages, elements)
60
+ def to_ast
61
+ elements.map(&:to_ast)
30
62
  end
31
63
  end
32
64
  end
@@ -0,0 +1,86 @@
1
+ require "json"
2
+
3
+ module Formalist
4
+ module RichText
5
+
6
+ # Our input data looks like this example, which consists of 3 elements:
7
+ #
8
+ # 1. A text line
9
+ # 2. embedded form data
10
+ # 3. Another text line
11
+ #
12
+ # [
13
+ # ["block",["unstyled","b14hd",[["inline",[[],"Before!"]]]]],
14
+ # ["block",["atomic","48b4f",[["entity",["formalist","1","IMMUTABLE",{"name":"image_with_caption","label":"Image with caption","data":{"image_id":"5678","caption":"Large panda"}},[["inline",[[],"¶"]]]]]]]],
15
+ # ["block",["unstyled","aivqi",[["inline",[[],"After!"]]]]]
16
+ # ]
17
+ #
18
+ # We want to intercept the embededed form data and transform them into full
19
+ # form ASTs, complete with validation messages.
20
+
21
+ class EmbeddedFormCompiler
22
+ attr_reader :embedded_forms
23
+
24
+ def initialize(embedded_form_collection)
25
+ @embedded_forms = embedded_form_collection
26
+ end
27
+
28
+ def call(ast)
29
+ return ast if ast.nil?
30
+
31
+ ast = ast.is_a?(String) ? JSON.parse(ast) : ast
32
+
33
+ ast.map { |node| visit(node) }
34
+ end
35
+ alias_method :[], :call
36
+
37
+ private
38
+
39
+ def visit(node)
40
+ name, nodes = node
41
+
42
+ handler = :"visit_#{name}"
43
+
44
+ if respond_to?(handler, true)
45
+ send(handler, nodes)
46
+ else
47
+ [name, nodes]
48
+ end
49
+ end
50
+
51
+ # We need to visit blocks in order to get to the formalist entities nested within them
52
+ def visit_block(node)
53
+ type, id, children = node
54
+
55
+ ["block", [type, id, children.map { |child| visit(child) }]]
56
+ end
57
+
58
+ def visit_entity(node)
59
+ type, key, mutability, entity_data, children = node
60
+
61
+ return ["entity", node] unless type == "formalist"
62
+
63
+ embedded_form = embedded_forms[entity_data["name"]]
64
+
65
+ compiled_entity_data = entity_data.merge(
66
+ "label" => embedded_form.label,
67
+ "form" => prepare_form_ast(embedded_form, entity_data["data"])
68
+ )
69
+
70
+ ["entity", [type, key, mutability, compiled_entity_data, children]]
71
+ end
72
+
73
+ def prepare_form_ast(embedded_form, data)
74
+ # Run the raw data through the validation schema
75
+ validation = embedded_form.schema.(data)
76
+
77
+ # And then through the embedded form's input processor (which may add
78
+ # extra system-generated information necessary for the form to render
79
+ # fully)
80
+ input = embedded_form.input_processor.(validation.to_h)
81
+
82
+ embedded_form.form.fill(input: input, errors: validation.messages).to_ast
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ require "dry-container"
2
+ require "formalist/rich_text/embedded_forms_container/registration"
3
+
4
+ module Formalist
5
+ module RichText
6
+ class EmbeddedFormsContainer
7
+ module Mixin
8
+ def self.included(base)
9
+ base.class_eval do
10
+ include ::Dry::Container::Mixin
11
+ include Methods
12
+ end
13
+ end
14
+
15
+ def self.extended(base)
16
+ base.class_eval do
17
+ extend ::Dry::Container::Mixin
18
+ extend Methods
19
+ end
20
+ end
21
+
22
+ module Methods
23
+ def resolve(key)
24
+ super(key.to_s)
25
+ end
26
+
27
+ def register(key, **attrs)
28
+ super(key.to_s, Registration.new(attrs))
29
+ end
30
+
31
+ def to_h
32
+ keys.each_with_object({}) { |key, output|
33
+ output[key] = self[key].to_h
34
+ }
35
+ end
36
+
37
+ # TODO: methods to return filtered sets of registrations
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ module Formalist
2
+ module RichText
3
+ class EmbeddedFormsContainer
4
+ class Registration
5
+ DEFAULT_INPUT_PROCESSOR = -> input { input }.freeze
6
+
7
+ attr_reader :label
8
+ attr_reader :form
9
+ attr_reader :schema
10
+ attr_reader :input_processor
11
+
12
+ def initialize(label:, form:, schema:, input_processor: DEFAULT_INPUT_PROCESSOR)
13
+ @label = label
14
+ @form = form
15
+ @schema = schema
16
+ @input_processor = input_processor
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ label: label,
22
+ form: form,
23
+ schema: schema,
24
+ input_processor: input_processor,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ require "formalist/rich_text/embedded_forms_container/mixin"
2
+
3
+ module Formalist
4
+ module RichText
5
+ class EmbeddedFormsContainer
6
+ include Mixin
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module Formalist
2
+ module RichText
3
+ module Rendering
4
+ class EmbeddedFormRenderer
5
+ attr_reader :container
6
+ attr_reader :options
7
+
8
+ def initialize(container = {}, **options)
9
+ @container = container
10
+ @options = options
11
+ end
12
+
13
+ def call(form_data)
14
+ type, data = form_data.values_at(:name, :data)
15
+
16
+ if container.key?(type)
17
+ container[type].(data, options)
18
+ else
19
+ ""
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,100 @@
1
+ module Formalist
2
+ module RichText
3
+ module Rendering
4
+ class HTMLCompiler
5
+ EMBEDDED_FORM_TYPE = "formalist".freeze
6
+ LIST_ITEM_TYPES = %w[unordered-list-item ordered-list-item].freeze
7
+
8
+ attr_reader :html_renderer
9
+ attr_reader :embedded_form_renderer
10
+
11
+ def initialize(html_renderer:, embedded_form_renderer:)
12
+ @html_renderer = html_renderer
13
+ @embedded_form_renderer = embedded_form_renderer
14
+ end
15
+
16
+ def call(ast)
17
+ html_renderer.nodes(wrap_lists(ast)) do |node|
18
+ visit(node)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def visit(node)
25
+ type, content = node
26
+
27
+ send(:"visit_#{type}", content)
28
+ end
29
+
30
+ def visit_block(data)
31
+ type, key, children = data
32
+
33
+ html_renderer.block(type, key, wrap_lists(children)) do |child|
34
+ visit(child)
35
+ end
36
+ end
37
+
38
+ def visit_wrapper(data)
39
+ type, children = data
40
+
41
+ html_renderer.wrapper(type, children) do |child|
42
+ visit(child)
43
+ end
44
+ end
45
+
46
+ def visit_inline(data)
47
+ styles, text = data
48
+
49
+ html_renderer.inline(styles, text)
50
+ end
51
+
52
+ def visit_entity(data)
53
+ type, key, _mutability, data, children = data
54
+
55
+ # FIXME
56
+ # Temporary fix to handle data that comes through with keys as
57
+ # strings instead of symbols
58
+ data = data.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
59
+
60
+ if type == EMBEDDED_FORM_TYPE
61
+ embedded_form_renderer.(data)
62
+ else
63
+ html_renderer.entity(type, key, data, wrap_lists(children)) do |child|
64
+ visit(child)
65
+ end
66
+ end
67
+ end
68
+
69
+ def wrap_lists(nodes)
70
+ chunked = nodes.chunk do |node|
71
+ type, content = node
72
+
73
+ if type == "block"
74
+ content[0] # return the block's own type
75
+ else
76
+ type
77
+ end
78
+ end
79
+
80
+ chunked.inject([]) { |output, (type, chunk)|
81
+ if list_item?(type)
82
+ output << convert_to_wrapper_node(type, chunk)
83
+ else
84
+ # Flatten again by appending chunk onto array
85
+ output + chunk
86
+ end
87
+ }
88
+ end
89
+
90
+ def convert_to_wrapper_node(type, children)
91
+ ["wrapper", [type, children]]
92
+ end
93
+
94
+ def list_item?(type)
95
+ LIST_ITEM_TYPES.include?(type)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end