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.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +105 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +328 -0
- data/bin/console +11 -0
- data/bin/render +68 -0
- data/lib/proforma.rb +38 -0
- data/lib/proforma/compiling.rb +12 -0
- data/lib/proforma/compiling/aggregation.rb +62 -0
- data/lib/proforma/compiling/compilable.rb +21 -0
- data/lib/proforma/compiling/counter.rb +35 -0
- data/lib/proforma/core_ext/hash.rb +21 -0
- data/lib/proforma/document.rb +38 -0
- data/lib/proforma/hash_evaluator.rb +40 -0
- data/lib/proforma/model_factory.rb +38 -0
- data/lib/proforma/modeling.rb +21 -0
- data/lib/proforma/modeling/banner.rb +64 -0
- data/lib/proforma/modeling/collection.rb +34 -0
- data/lib/proforma/modeling/data_table.rb +117 -0
- data/lib/proforma/modeling/data_table/aggregator.rb +43 -0
- data/lib/proforma/modeling/data_table/column.rb +94 -0
- data/lib/proforma/modeling/generic_container.rb +57 -0
- data/lib/proforma/modeling/grouping.rb +40 -0
- data/lib/proforma/modeling/header.rb +18 -0
- data/lib/proforma/modeling/pane.rb +40 -0
- data/lib/proforma/modeling/pane/column.rb +68 -0
- data/lib/proforma/modeling/pane/line.rb +42 -0
- data/lib/proforma/modeling/separator.rb +19 -0
- data/lib/proforma/modeling/spacer.rb +19 -0
- data/lib/proforma/modeling/table.rb +52 -0
- data/lib/proforma/modeling/table/cell.rb +49 -0
- data/lib/proforma/modeling/table/row.rb +24 -0
- data/lib/proforma/modeling/table/section.rb +23 -0
- data/lib/proforma/modeling/text.rb +37 -0
- data/lib/proforma/modeling/types.rb +10 -0
- data/lib/proforma/modeling/types/align.rb +22 -0
- data/lib/proforma/plain_text_renderer.rb +106 -0
- data/lib/proforma/prototype.rb +28 -0
- data/lib/proforma/template.rb +65 -0
- data/lib/proforma/type_factory.rb +44 -0
- data/lib/proforma/version.rb +12 -0
- data/proforma.gemspec +34 -0
- data/spec/fixtures/snapshots/custom_table.yml +55 -0
- data/spec/fixtures/snapshots/user_details.yml +101 -0
- data/spec/fixtures/snapshots/user_list.yml +143 -0
- data/spec/proforma/hash_evaluator_spec.rb +26 -0
- data/spec/proforma/modeling/table/cell_spec.rb +26 -0
- data/spec/proforma/modeling/table/row_spec.rb +42 -0
- data/spec/proforma_spec.rb +230 -0
- data/spec/spec_helper.rb +25 -0
- 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
|