proforma 1.0.0.pre.alpha

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +8 -0
  3. data/.gitignore +4 -0
  4. data/.rubocop.yml +11 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +20 -0
  7. data/CHANGELOG.md +7 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +105 -0
  10. data/Guardfile +16 -0
  11. data/LICENSE +7 -0
  12. data/README.md +328 -0
  13. data/bin/console +11 -0
  14. data/bin/render +68 -0
  15. data/lib/proforma.rb +38 -0
  16. data/lib/proforma/compiling.rb +12 -0
  17. data/lib/proforma/compiling/aggregation.rb +62 -0
  18. data/lib/proforma/compiling/compilable.rb +21 -0
  19. data/lib/proforma/compiling/counter.rb +35 -0
  20. data/lib/proforma/core_ext/hash.rb +21 -0
  21. data/lib/proforma/document.rb +38 -0
  22. data/lib/proforma/hash_evaluator.rb +40 -0
  23. data/lib/proforma/model_factory.rb +38 -0
  24. data/lib/proforma/modeling.rb +21 -0
  25. data/lib/proforma/modeling/banner.rb +64 -0
  26. data/lib/proforma/modeling/collection.rb +34 -0
  27. data/lib/proforma/modeling/data_table.rb +117 -0
  28. data/lib/proforma/modeling/data_table/aggregator.rb +43 -0
  29. data/lib/proforma/modeling/data_table/column.rb +94 -0
  30. data/lib/proforma/modeling/generic_container.rb +57 -0
  31. data/lib/proforma/modeling/grouping.rb +40 -0
  32. data/lib/proforma/modeling/header.rb +18 -0
  33. data/lib/proforma/modeling/pane.rb +40 -0
  34. data/lib/proforma/modeling/pane/column.rb +68 -0
  35. data/lib/proforma/modeling/pane/line.rb +42 -0
  36. data/lib/proforma/modeling/separator.rb +19 -0
  37. data/lib/proforma/modeling/spacer.rb +19 -0
  38. data/lib/proforma/modeling/table.rb +52 -0
  39. data/lib/proforma/modeling/table/cell.rb +49 -0
  40. data/lib/proforma/modeling/table/row.rb +24 -0
  41. data/lib/proforma/modeling/table/section.rb +23 -0
  42. data/lib/proforma/modeling/text.rb +37 -0
  43. data/lib/proforma/modeling/types.rb +10 -0
  44. data/lib/proforma/modeling/types/align.rb +22 -0
  45. data/lib/proforma/plain_text_renderer.rb +106 -0
  46. data/lib/proforma/prototype.rb +28 -0
  47. data/lib/proforma/template.rb +65 -0
  48. data/lib/proforma/type_factory.rb +44 -0
  49. data/lib/proforma/version.rb +12 -0
  50. data/proforma.gemspec +34 -0
  51. data/spec/fixtures/snapshots/custom_table.yml +55 -0
  52. data/spec/fixtures/snapshots/user_details.yml +101 -0
  53. data/spec/fixtures/snapshots/user_list.yml +143 -0
  54. data/spec/proforma/hash_evaluator_spec.rb +26 -0
  55. data/spec/proforma/modeling/table/cell_spec.rb +26 -0
  56. data/spec/proforma/modeling/table/row_spec.rb +42 -0
  57. data/spec/proforma_spec.rb +230 -0
  58. data/spec/spec_helper.rb +25 -0
  59. metadata +211 -0
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'proforma'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require 'pry'
11
+ Pry.start
data/bin/render ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'proforma'
6
+ require 'fileutils'
7
+ require 'yaml'
8
+ require 'pry'
9
+
10
+ def read_yaml(filename)
11
+ # rubocop:disable Security/YAMLLoad
12
+ YAML.load(read(filename))
13
+ # rubocop:enable Security/YAMLLoad
14
+ end
15
+
16
+ def read(filename)
17
+ File.open(filename, 'r:bom|utf-8').read
18
+ end
19
+
20
+ jobs_dir = File.join('tmp', 'jobs')
21
+ output_dir = File.join('tmp', 'jobs_output')
22
+
23
+ jobs = Dir[File.join(jobs_dir, '*.yml')]
24
+
25
+ if jobs.empty?
26
+ puts "No jobs detected in the path: #{jobs_dir}"
27
+ puts '--------------------------------------------------------------'
28
+ puts "To use, place YAML files into #{jobs_dir} directory."
29
+ puts 'You can find example YAML files in: spec/fixtures/snapshots'
30
+ puts '--------------------------------------------------------------'
31
+
32
+ exit
33
+ end
34
+
35
+ jobs.each do |job|
36
+ job_name = File.basename(job, File.extname(job))
37
+
38
+ puts "Rendering: #{job_name} (#{job})"
39
+
40
+ contents = read_yaml(job)
41
+ data = contents['data']
42
+ template = contents['template']
43
+ evaluator = contents['evaluator'] || Proforma::HashEvaluator.new
44
+ renderer = contents['renderer'] || Proforma::PlainTextRenderer.new
45
+ job_output_dir = File.join(output_dir, job_name)
46
+
47
+ FileUtils.mkdir_p(job_output_dir)
48
+
49
+ outputs = Proforma.render(
50
+ data,
51
+ template,
52
+ evaluator: evaluator,
53
+ renderer: renderer
54
+ )
55
+
56
+ outputs.each_with_index do |output, index|
57
+ name_without_extension = [
58
+ index.to_s,
59
+ output.title.to_s
60
+ ].reject(&:empty?).join('.')
61
+
62
+ filename = "#{name_without_extension}#{output.extension}"
63
+
64
+ IO.write(File.join(job_output_dir, filename), output.contents)
65
+ end
66
+
67
+ puts "Done Rendering: #{job} (output can be found in: #{job_output_dir})"
68
+ end
data/lib/proforma.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'acts_as_hashable'
11
+ require 'bigdecimal'
12
+ require 'forwardable'
13
+ require 'stringio'
14
+
15
+ # Monkey-patching core libaries
16
+ require_relative 'proforma/core_ext/hash'
17
+ Hash.include ::Proforma::CoreExt::Hash
18
+
19
+ require_relative 'proforma/compiling'
20
+ require_relative 'proforma/document'
21
+ require_relative 'proforma/hash_evaluator'
22
+ require_relative 'proforma/modeling'
23
+ require_relative 'proforma/plain_text_renderer'
24
+ require_relative 'proforma/prototype'
25
+ require_relative 'proforma/template'
26
+ require_relative 'proforma/type_factory'
27
+ require_relative 'proforma/model_factory'
28
+
29
+ # The top-level API that should be seen as the main entry point into this library.
30
+ module Proforma
31
+ class << self
32
+ def render(data, template, evaluator: HashEvaluator.new, renderer: PlainTextRenderer.new)
33
+ Template.make(template)
34
+ .compile(data, evaluator)
35
+ .map { |prototype| renderer.render(prototype) }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'compiling/aggregation'
11
+ require_relative 'compiling/compilable'
12
+ require_relative 'compiling/counter'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ module Compiling
12
+ # This class is a group of aggregators that knows how to process records.
13
+ class Aggregation
14
+ attr_reader :aggregators, :evaluator
15
+
16
+ def initialize(aggregators, evaluator)
17
+ raise ArgumentError, 'evaluator is required' unless evaluator
18
+
19
+ @aggregators = Array(aggregators)
20
+ @counters = {}
21
+ @evaluator = evaluator
22
+ end
23
+
24
+ def add(records)
25
+ records.each do |record|
26
+ aggregators.each do |aggregator|
27
+ property = aggregator.property
28
+ value = evaluator.value(record, property)
29
+ name = aggregator.name
30
+
31
+ entry(name).add(value)
32
+ end
33
+ end
34
+
35
+ self
36
+ end
37
+
38
+ def to_hash
39
+ aggregators.map { |aggregator| execute(aggregator) }.to_h
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :counters
45
+
46
+ def entry(name)
47
+ counters[name.to_s] ||= Counter.new
48
+ end
49
+
50
+ def execute(aggregator)
51
+ name = aggregator.name
52
+ function = aggregator.function
53
+
54
+ raise ArgumentError, "bad func: #{function}" unless entry(name).respond_to?(function)
55
+
56
+ value = entry(name).send(function)
57
+
58
+ [name, value.to_s('f')]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ module Compiling
12
+ # This mix-in provides common methods for model compilation.
13
+ module Compilable
14
+ private
15
+
16
+ def array(data)
17
+ data.is_a?(Hash) ? [data] : Array(data)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ module Compiling
12
+ # A counter is the actual underlying data structure for aggregation.
13
+ class Counter
14
+ attr_reader :count, :sum
15
+
16
+ def initialize
17
+ @count = 0
18
+ @sum = BigDecimal(0)
19
+ end
20
+
21
+ def add(value)
22
+ parsed_value = value.to_s.empty? ? 0 : BigDecimal(value.to_s)
23
+
24
+ @count += 1
25
+ @sum += parsed_value
26
+
27
+ self
28
+ end
29
+
30
+ def ave
31
+ sum / count
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ module CoreExt
12
+ # Monkey-patches for the core Hash class. These will be manually mixed in separately.
13
+ module Hash
14
+ unless method_defined?(:symbolize_keys)
15
+ def symbolize_keys
16
+ map { |k, v| [k.to_sym, v] }.to_h
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ # A rendering engine will output one or more of these objects. It is the final realization
12
+ # of the compilation + rendering process.
13
+ class Document
14
+ acts_as_hashable
15
+
16
+ attr_reader :contents, :extension, :title
17
+
18
+ def initialize(contents: nil, extension: '', title: '')
19
+ @contents = contents
20
+ @extension = extension
21
+ @title = title
22
+
23
+ freeze
24
+ end
25
+
26
+ def eql?(other)
27
+ return false unless other.is_a?(self.class)
28
+
29
+ contents == other.contents &&
30
+ extension == other.extension &&
31
+ title == other.title
32
+ end
33
+
34
+ def ==(other)
35
+ eql?(other)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ # This class will serve as the evaluator that gets shipped with the base framework.
12
+ # Other packages can extend or create their own and plug them into the rendering
13
+ # pipeline.
14
+ # This, being a prototype for customizable evaluators, just provides basic evaluation:
15
+ # - it can only handle hashes for value extraction
16
+ # - if text is prefixed with a dollar sign and colon then it means it will be dynamically
17
+ # evaluated against the record. For example: $:id
18
+ class HashEvaluator
19
+ PROPERTY_PREFIX = '$:'
20
+
21
+ def value(object, expression)
22
+ return object if expression.to_s.empty?
23
+ return nil unless object.is_a?(Hash)
24
+
25
+ if object.key?(expression.to_s)
26
+ object[expression.to_s]
27
+ elsif object.key?(expression.to_s.to_sym)
28
+ object[expression.to_s.to_sym]
29
+ end
30
+ end
31
+
32
+ def text(object, expression)
33
+ if expression.to_s.start_with?(PROPERTY_PREFIX)
34
+ value(object, expression.to_s[2..-1])
35
+ else
36
+ expression
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ # This class serves as a singleton that can make Proforma::Modeling components.
12
+ class ModelFactory
13
+ REGISTRY = {
14
+ 'Banner': Modeling::Banner,
15
+ 'Collection': Modeling::Collection,
16
+ 'DataTable': Modeling::DataTable,
17
+ 'Grouping': Modeling::Grouping,
18
+ 'Header': Modeling::Header,
19
+ 'Pane': Modeling::Pane,
20
+ 'Separator': Modeling::Separator,
21
+ 'Spacer': Modeling::Spacer,
22
+ 'Table': Modeling::Table,
23
+ 'Text': Modeling::Text
24
+ }.freeze
25
+
26
+ class << self
27
+ extend Forwardable
28
+
29
+ def_delegators :factory, :array, :make
30
+
31
+ private
32
+
33
+ def factory
34
+ @factory ||= TypeFactory.new(REGISTRY)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'modeling/generic_container'
11
+ require_relative 'modeling/types'
12
+ require_relative 'modeling/banner'
13
+ require_relative 'modeling/collection'
14
+ require_relative 'modeling/data_table'
15
+ require_relative 'modeling/grouping'
16
+ require_relative 'modeling/header'
17
+ require_relative 'modeling/pane'
18
+ require_relative 'modeling/separator'
19
+ require_relative 'modeling/spacer'
20
+ require_relative 'modeling/table'
21
+ require_relative 'modeling/text'
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Proforma
11
+ module Modeling
12
+ # A Banner is a specific type of header that is comprised of an image and some text.
13
+ # Both the image and text could be optional and, like all modeling components,
14
+ # it is up to the rendering engine how to render it.
15
+ class Banner
16
+ include Compiling::Compilable
17
+ acts_as_hashable
18
+
19
+ attr_writer :details, :image_height, :image_width, :title
20
+
21
+ attr_accessor :image
22
+
23
+ def initialize(
24
+ details: '',
25
+ image: nil,
26
+ image_height: nil,
27
+ image_width: nil,
28
+ title: ''
29
+ )
30
+ @details = details
31
+ @image = image
32
+ @image_height = image_height
33
+ @image_width = image_width
34
+ @title = title
35
+ end
36
+
37
+ def details
38
+ @details.to_s
39
+ end
40
+
41
+ def title
42
+ @title.to_s
43
+ end
44
+
45
+ def image_height
46
+ @image_height ? @image_height.to_f : nil
47
+ end
48
+
49
+ def image_width
50
+ @image_width ? @image_width.to_f : nil
51
+ end
52
+
53
+ def compile(data, evaluator)
54
+ self.class.new(
55
+ details: evaluator.text(data, details),
56
+ image: evaluator.value(data, image),
57
+ image_height: image_height,
58
+ image_width: image_width,
59
+ title: evaluator.text(data, title)
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end