proforma 1.0.0.pre.alpha

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