Commit 5c94ef05 authored by Nikola Milojevic's avatar Nikola Milojevic

Merge branch 'extract-template-parser-module' into 'master'

Extract changelog template parser to new module

See merge request gitlab-org/gitlab!63864
parents 6bf50afb fa4f61c0
...@@ -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