Commit fa4f61c0 authored by Piotr Stankowski's avatar Piotr Stankowski Committed by Nikola Milojevic

Extract changelog template parser to new module

Template parser that was added with changelog generator can be used
outside of changelogs, for example for merge commit templates.
This commit simply refactors the generic parts of template parser and
moves them out to module Gitlab::TemplateParser.
parent 581f7a4b
...@@ -52,7 +52,12 @@ module Gitlab ...@@ -52,7 +52,12 @@ module Gitlab
end end
if (template = hash['template']) if (template = hash['template'])
config.template = Parser.new.parse_and_transform(template) config.template =
begin
TemplateParser::Parser.new.parse_and_transform(template)
rescue TemplateParser::Error => e
raise Error, e.message
end
end end
if (categories = hash['categories']) if (categories = hash['categories'])
...@@ -73,7 +78,12 @@ module Gitlab ...@@ -73,7 +78,12 @@ module Gitlab
def initialize(project) def initialize(project)
@project = project @project = project
@date_format = DEFAULT_DATE_FORMAT @date_format = DEFAULT_DATE_FORMAT
@template = Parser.new.parse_and_transform(DEFAULT_TEMPLATE) @template =
begin
TemplateParser::Parser.new.parse_and_transform(DEFAULT_TEMPLATE)
rescue TemplateParser::Error => e
raise Error, e.message
end
@categories = {} @categories = {}
@tag_regex = DEFAULT_TAG_REGEX @tag_regex = DEFAULT_TAG_REGEX
end end
......
...@@ -54,7 +54,7 @@ module Gitlab ...@@ -54,7 +54,7 @@ module Gitlab
end end
def to_markdown def to_markdown
state = EvalState.new state = TemplateParser::EvalState.new
data = { 'categories' => entries_for_template } data = { 'categories' => entries_for_template }
# While not critical, we would like release sections to be separated by # While not critical, we would like release sections to be separated by
...@@ -63,7 +63,12 @@ module Gitlab ...@@ -63,7 +63,12 @@ module Gitlab
# #
# Since it can be a bit tricky to get this right in a template, we # Since it can be a bit tricky to get this right in a template, we
# enforce an empty line separator ourselves. # enforce an empty line separator ourselves.
markdown = @config.template.evaluate(state, data).strip markdown =
begin
@config.template.evaluate(state, data).strip
rescue TemplateParser::ParseError => e
raise Error, e.message
end
# The release header can't be changed using the Liquid template, as we # The release header can't be changed using the Liquid template, as we
# need this to be in a known format. Without this restriction, we won't # need this to be in a known format. Without this restriction, we won't
......
# frozen_string_literal: true # frozen_string_literal: true
module Gitlab module Gitlab
module Changelog module TemplateParser
# AST nodes to evaluate when rendering a template. # AST nodes to evaluate when rendering a template.
# #
# Evaluating an AST is done by walking over the nodes and calling # Evaluating an AST is done by walking over the nodes and calling
......
# frozen_string_literal: true
module Gitlab
module TemplateParser
# An error raised when a template couldn't be rendered.
Error = Class.new(StandardError)
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Gitlab module Gitlab
module Changelog module TemplateParser
# A class for tracking state when evaluating a template # A class for tracking state when evaluating a template
class EvalState class EvalState
MAX_LOOPS = 4 MAX_LOOPS = 4
......
# frozen_string_literal: true # frozen_string_literal: true
module Gitlab module Gitlab
module Changelog module TemplateParser
# A parser for the template syntax used for generating changelogs. # A parser for a simple template syntax, used for example to generate changelogs.
# #
# As a quick primer on the template syntax, a basic template looks like # As a quick primer on the template syntax, a basic template looks like
# this: # this:
...@@ -166,7 +166,7 @@ module Gitlab ...@@ -166,7 +166,7 @@ module Gitlab
def parse_and_transform(input) def parse_and_transform(input)
AST::Transformer.new.apply(parse(input)) AST::Transformer.new.apply(parse(input))
rescue Parslet::ParseFailed => ex rescue Parslet::ParseFailed => ex
# We raise a custom error so it's easier to catch different changelog # We raise a custom error so it's easier to catch different parser
# related errors. In addition, this ensures the caller of this method # related errors. In addition, this ensures the caller of this method
# doesn't depend on a Parslet specific error class. # doesn't depend on a Parslet specific error class.
raise Error, "Failed to parse the template: #{ex.message}" raise Error, "Failed to parse the template: #{ex.message}"
......
...@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Changelog::Config do ...@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Changelog::Config do
expect(config.date_format).to eq('foo') expect(config.date_format).to eq('foo')
expect(config.template) expect(config.template)
.to be_instance_of(Gitlab::Changelog::AST::Expressions) .to be_instance_of(Gitlab::TemplateParser::AST::Expressions)
expect(config.categories).to eq({ 'foo' => 'bar' }) expect(config.categories).to eq({ 'foo' => 'bar' })
expect(config.tag_regex).to eq('foo') expect(config.tag_regex).to eq('foo')
...@@ -53,6 +53,16 @@ RSpec.describe Gitlab::Changelog::Config do ...@@ -53,6 +53,16 @@ RSpec.describe Gitlab::Changelog::Config do
expect { described_class.from_hash(project, 'categories' => 10) } expect { described_class.from_hash(project, 'categories' => 10) }
.to raise_error(Gitlab::Changelog::Error) .to raise_error(Gitlab::Changelog::Error)
end end
it 'raises a Gitlab::Changelog::Error when the template is invalid' do
invalid_template = <<~TPL
{% each {{foo}} %}
{% end %}
TPL
expect { described_class.from_hash(project, 'template' => invalid_template) }
.to raise_error(Gitlab::Changelog::Error)
end
end end
describe '#contributor?' do describe '#contributor?' do
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Changelog::AST::Identifier do RSpec.describe Gitlab::TemplateParser::AST::Identifier do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates a selector' do it 'evaluates a selector' do
...@@ -26,8 +26,8 @@ RSpec.describe Gitlab::Changelog::AST::Identifier do ...@@ -26,8 +26,8 @@ RSpec.describe Gitlab::Changelog::AST::Identifier do
end end
end end
RSpec.describe Gitlab::Changelog::AST::Integer do RSpec.describe Gitlab::TemplateParser::AST::Integer do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates a selector' do it 'evaluates a selector' do
...@@ -44,33 +44,33 @@ RSpec.describe Gitlab::Changelog::AST::Integer do ...@@ -44,33 +44,33 @@ RSpec.describe Gitlab::Changelog::AST::Integer do
end end
end end
RSpec.describe Gitlab::Changelog::AST::Selector do RSpec.describe Gitlab::TemplateParser::AST::Selector do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
let(:data) { { 'numbers' => [10] } } let(:data) { { 'numbers' => [10] } }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates a selector' do it 'evaluates a selector' do
ident = Gitlab::Changelog::AST::Identifier.new('numbers') ident = Gitlab::TemplateParser::AST::Identifier.new('numbers')
int = Gitlab::Changelog::AST::Integer.new(0) int = Gitlab::TemplateParser::AST::Integer.new(0)
expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10) expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10)
end end
it 'evaluates a selector that returns nil' do it 'evaluates a selector that returns nil' do
int = Gitlab::Changelog::AST::Integer.new(0) int = Gitlab::TemplateParser::AST::Integer.new(0)
expect(described_class.new([int]).evaluate(state, data)).to be_nil expect(described_class.new([int]).evaluate(state, data)).to be_nil
end end
end end
end end
RSpec.describe Gitlab::Changelog::AST::Variable do RSpec.describe Gitlab::TemplateParser::AST::Variable do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
let(:data) { { 'numbers' => [10] } } let(:data) { { 'numbers' => [10] } }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates a variable' do it 'evaluates a variable' do
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{{numbers.0}}') .parse_and_transform('{{numbers.0}}')
.nodes[0] .nodes[0]
...@@ -80,26 +80,26 @@ RSpec.describe Gitlab::Changelog::AST::Variable do ...@@ -80,26 +80,26 @@ RSpec.describe Gitlab::Changelog::AST::Variable do
it 'evaluates an undefined variable' do it 'evaluates an undefined variable' do
node = node =
Gitlab::Changelog::Parser.new.parse_and_transform('{{foobar}}').nodes[0] Gitlab::TemplateParser::Parser.new.parse_and_transform('{{foobar}}').nodes[0]
expect(node.evaluate(state, data)).to eq('') expect(node.evaluate(state, data)).to eq('')
end end
it 'evaluates the special variable "it"' do it 'evaluates the special variable "it"' do
node = node =
Gitlab::Changelog::Parser.new.parse_and_transform('{{it}}').nodes[0] Gitlab::TemplateParser::Parser.new.parse_and_transform('{{it}}').nodes[0]
expect(node.evaluate(state, data)).to eq(data.to_s) expect(node.evaluate(state, data)).to eq(data.to_s)
end end
end end
end end
RSpec.describe Gitlab::Changelog::AST::Expressions do RSpec.describe Gitlab::TemplateParser::AST::Expressions do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates all expressions' do it 'evaluates all expressions' do
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{{number}}foo') .parse_and_transform('{{number}}foo')
...@@ -108,8 +108,8 @@ RSpec.describe Gitlab::Changelog::AST::Expressions do ...@@ -108,8 +108,8 @@ RSpec.describe Gitlab::Changelog::AST::Expressions do
end end
end end
RSpec.describe Gitlab::Changelog::AST::Text do RSpec.describe Gitlab::TemplateParser::AST::Text do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'returns the text' do it 'returns the text' do
...@@ -118,12 +118,12 @@ RSpec.describe Gitlab::Changelog::AST::Text do ...@@ -118,12 +118,12 @@ RSpec.describe Gitlab::Changelog::AST::Text do
end end
end end
RSpec.describe Gitlab::Changelog::AST::If do RSpec.describe Gitlab::TemplateParser::AST::If do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates a truthy if expression without an else clause' do it 'evaluates a truthy if expression without an else clause' do
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{% if thing %}foo{% end %}') .parse_and_transform('{% if thing %}foo{% end %}')
.nodes[0] .nodes[0]
...@@ -132,7 +132,7 @@ RSpec.describe Gitlab::Changelog::AST::If do ...@@ -132,7 +132,7 @@ RSpec.describe Gitlab::Changelog::AST::If do
end end
it 'evaluates a falsy if expression without an else clause' do it 'evaluates a falsy if expression without an else clause' do
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{% if thing %}foo{% end %}') .parse_and_transform('{% if thing %}foo{% end %}')
.nodes[0] .nodes[0]
...@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Changelog::AST::If do ...@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Changelog::AST::If do
end end
it 'evaluates a falsy if expression with an else clause' do it 'evaluates a falsy if expression with an else clause' do
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{% if thing %}foo{% else %}bar{% end %}') .parse_and_transform('{% if thing %}foo{% else %}bar{% end %}')
.nodes[0] .nodes[0]
...@@ -177,13 +177,13 @@ RSpec.describe Gitlab::Changelog::AST::If do ...@@ -177,13 +177,13 @@ RSpec.describe Gitlab::Changelog::AST::If do
end end
end end
RSpec.describe Gitlab::Changelog::AST::Each do RSpec.describe Gitlab::TemplateParser::AST::Each do
let(:state) { Gitlab::Changelog::EvalState.new } let(:state) { Gitlab::TemplateParser::EvalState.new }
describe '#evaluate' do describe '#evaluate' do
it 'evaluates the expression' do it 'evaluates the expression' do
data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] } data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] }
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{% each animals %}{{name}}{% end %}') .parse_and_transform('{% each animals %}{{name}}{% end %}')
.nodes[0] .nodes[0]
...@@ -193,7 +193,7 @@ RSpec.describe Gitlab::Changelog::AST::Each do ...@@ -193,7 +193,7 @@ RSpec.describe Gitlab::Changelog::AST::Each do
it 'returns an empty string when the input is not a collection' do it 'returns an empty string when the input is not a collection' do
data = { 'animals' => 10 } data = { 'animals' => 10 }
node = Gitlab::Changelog::Parser node = Gitlab::TemplateParser::Parser
.new .new
.parse_and_transform('{% each animals %}{{name}}{% end %}') .parse_and_transform('{% each animals %}{{name}}{% end %}')
.nodes[0] .nodes[0]
...@@ -237,10 +237,10 @@ RSpec.describe Gitlab::Changelog::AST::Each do ...@@ -237,10 +237,10 @@ RSpec.describe Gitlab::Changelog::AST::Each do
TPL TPL
node = node =
Gitlab::Changelog::Parser.new.parse_and_transform(template).nodes[0] Gitlab::TemplateParser::Parser.new.parse_and_transform(template).nodes[0]
expect { node.evaluate(state, data) } expect { node.evaluate(state, data) }
.to raise_error(Gitlab::Changelog::Error) .to raise_error(Gitlab::TemplateParser::Error)
end end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Changelog::Parser do RSpec.describe Gitlab::TemplateParser::Parser do
let(:parser) { described_class.new } let(:parser) { described_class.new }
describe '#root' do describe '#root' do
...@@ -67,12 +67,12 @@ RSpec.describe Gitlab::Changelog::Parser do ...@@ -67,12 +67,12 @@ RSpec.describe Gitlab::Changelog::Parser do
it 'parses and transforms a template' do it 'parses and transforms a template' do
node = parser.parse_and_transform('foo') node = parser.parse_and_transform('foo')
expect(node).to be_instance_of(Gitlab::Changelog::AST::Expressions) expect(node).to be_instance_of(Gitlab::TemplateParser::AST::Expressions)
end end
it 'raises parsing errors using a custom error class' do it 'raises parsing errors using a custom error class' do
expect { parser.parse_and_transform('{% each') } expect { parser.parse_and_transform('{% each') }
.to raise_error(Gitlab::Changelog::Error) .to raise_error(Gitlab::TemplateParser::Error)
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment