singer 1.0.0 → 2.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d116f657ac1b8d3b57e540ae37bf0d74c21fe357938fbb6aa33309663b9add5
4
- data.tar.gz: 7854c981adfd45a2989647f6302022ff0b40e66b47960c2fc536cd428dc99c93
3
+ metadata.gz: b91934a916b675cfddecdac26f48673a77e9e2f014ae94a7db7623df0adaac67
4
+ data.tar.gz: f8a8384bdbba9f096d303e91fbd68a2b1063239d89e5f072d1e8e551cee1b7d2
5
5
  SHA512:
6
- metadata.gz: 933cc4b306f621d57598f0b1d5f72993fd6c25afea2496aa89f088847b42374b8a01bf68d709a2c3f154455b02b6158f2072677ec23b55ff1b359020c24cfbe5
7
- data.tar.gz: 0ed47b046d03641227f28faa499d9ea50cd80901ddf7efa75f5c1da4df07a54b5ad435bb338158047438175862c17b310e92d19ee94ecfafdf135d9810dce468
6
+ metadata.gz: 5a0d34e3e4ba0b18a55151f796f30d8b0bad715f74a084f37456a4cdebb74d8f66543db90003dc84877a19ef295f0664f1e5ba190cd137d67a670f8819802d01
7
+ data.tar.gz: d9245c0f2fcbc5671a078daf5bcf71977ba2ec661c1b599e0aedaa2e679948bc4d9ebad738a6302daefe515dae253a5d8d59d6b12b0779b7796b6c9bf52b35d2
data/README.md CHANGED
@@ -17,6 +17,53 @@ Run `singer --list-templates` to see available templates' names, both shipped wi
17
17
 
18
18
  For `PROJECT_NAME`, using `snake_case` is best, although an attempt will be made to understand `CamelCase`, too.
19
19
 
20
+ ## Writing your own templates
21
+
22
+ Many features of Singer will become clearer when seeing actual usage - please feel encouraged to examine the templates shipped with the gem (you can find them in `templates_from_gem` shown by the `singer --show-paths`).
23
+
24
+ ### Template location
25
+
26
+ User-provided templates should be placed in the directory named `templates_from_user` in the `singer --show-paths` output.
27
+ Each template resides in a subdirectory - the subdirectory's name becomes the template's name, and is not included in the generated paths (Singer will only replicate the directory structure _inside_ it).
28
+
29
+ ### Variables available to templates
30
+
31
+ Templates written in ERB or other templating mechanisms that execute Ruby code will have access to `Singer.configuration` object and its accessors. For example, the following code in a `my_template`'s file:
32
+ ```ruby
33
+ class <%= Singer.configuration.project_name_camelcase %>
34
+ ```
35
+ will, when Singer is called with `singer my_template foo_bar_baz`, result in this written to output file:
36
+ ```ruby
37
+ class FooBarBaz
38
+ ```
39
+
40
+ To see an up-to-date list of available variables, run `singer --list-variables`.
41
+
42
+ ### Using --dashes option
43
+
44
+ Since Ruby treats the `-` character in snakecased name as corresponding to module nesting in camelcased name, Singer also treats it in a special way. You can control this behavior with the `--dashes` option. This is how different values will affect `project_name_camelcase`:
45
+
46
+ ```bash
47
+ singer foo_bar-baz --dashes modularize # results in FooBar::Baz (default)
48
+ singer foo_bar-baz --dashes preserve # results in FooBar-baz
49
+ singer foo_bar-baz --dashes delete # results in FooBarbaz
50
+ ```
51
+
52
+ ### Special segments in template paths
53
+
54
+ Any of the variables above can be also used in paths/filenames of template files, capitalized and surrounded by double underscores.
55
+ So, if `my_template` has a file at this path:
56
+ ```
57
+ docs_for___PROJECT_NAME_SNAKECASE__/__PROJECT_NAME_CAMELCASE__ - humble beginnings.txt
58
+ ```
59
+ and you run `singer my_template alpha_beta`, a file named like this will be created:
60
+ ```
61
+ docs_for_alpha_beta/AlphaBeta - humble beginnings.txt
62
+ ```
63
+
64
+ Tiny caveat for completeness - while `template_file_name_actual` can be used inside template files, the `__TEMPLATE_FILE_NAME_ACTUAL__` will not be substituted in paths.
65
+ "Stack level too deep" and all that ;)
66
+
20
67
  ## Development
21
68
 
22
69
  ```bash
data/exe/singer CHANGED
@@ -2,5 +2,4 @@
2
2
 
3
3
  require 'singer'
4
4
 
5
- Singer::OptionParsing.configure(ARGV)
6
- Singer.generate
5
+ Singer.configure_and_generate(ARGV)
@@ -1,4 +1,16 @@
1
+ # main module, has `.configuration` reader
1
2
  module Singer
2
3
  # holds settings
3
- CONFIGURATION = Struct.new(:template_name, :camelcase_name, :snakecase_name).new
4
+ Configuration = Struct.new(
5
+ 'Configuration',
6
+ :template_name,
7
+ :project_name_original, :project_name_camelcase, :project_name_snakecase,
8
+ :template_file_name_original, :template_file_name_actual,
9
+ :dashes,
10
+ )
11
+ CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS = %w[template_file_name_actual].freeze
12
+
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
15
+ end
4
16
  end
@@ -0,0 +1,26 @@
1
+ module Singer
2
+ # transform between snakecase and camelcase, with some special rules
3
+ class NameTransformation
4
+ DASHES_OPTIONS = %w[modularize delete preserve].freeze
5
+
6
+ def self.encase_name(name)
7
+ Singer.configuration.project_name_original = name
8
+ Singer.configuration.project_name_snakecase = name.gsub(/(\w)([A-Z])/){ "#{$1}_#{$2}" }.gsub(/_+/, '_').downcase
9
+ camelcase = Singer.configuration.project_name_snakecase.split('_').map(&:capitalize).join
10
+ Singer.configuration.project_name_camelcase = treat_dashes_in_name(camelcase)
11
+ end
12
+
13
+ def self.treat_dashes_in_name(camelcase)
14
+ case Singer.configuration.dashes
15
+ when 'modularize'
16
+ camelcase.gsub(/([[:alnum:]])-+([[:alnum:]])/){ "#{$1}::#{$2.upcase}" }
17
+ when 'delete'
18
+ camelcase.delete('-')
19
+ when 'preserve'
20
+ camelcase
21
+ else
22
+ raise ParameterValueUnsupported, "Unsupported value #{Singer.configuration.dashes.inspect} for --dashes option"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,40 +1,80 @@
1
1
  module Singer
2
2
  # extracts what we need from ARGV
3
3
  class OptionParsing
4
- DEFAULT_TEMPLATE_NAME = 'tdd'.freeze
5
-
6
4
  def self.configure(argvies) # rubocop:disable Metrics/MethodLength
7
5
  options = {}
6
+ Singer.configuration.dashes = NameTransformation::DASHES_OPTIONS.first
7
+
8
8
  OptionParser.new do |parser|
9
+ parser.program_name = 'singer'
10
+ parser.version = Singer::VERSION
9
11
  parser.banner = <<~USAGE
10
- Usage: #{$PROGRAM_NAME} [OPTION]... [TEMPLATE_NAME] PROJECT_NAME
12
+ Usage: #{parser.program_name} [OPTION]... [TEMPLATE_NAME] PROJECT_NAME
11
13
  Generates a project from a multi-file template.
12
14
  PROJECT_NAME can be in snake_case or CamelCase.
13
15
 
14
16
  Options:
15
17
  USAGE
16
- parser.on('--list-templates', 'list all available templates') do
17
- list_all_templates
18
- Kernel.exit
19
- end
18
+ add_options_functional(parser)
19
+ add_options_listing(parser)
20
20
  end.parse!(argvies, into: options)
21
+ end
21
22
 
22
- raise NameMissingError, 'Missing mandatory argument PROJECT_NAME' if argvies.empty?
23
+ def self.add_options_functional(parser) # rubocop:disable Metrics/MethodLength
24
+ parser.on(
25
+ '-t', '--template-path=PATH', 'load a template from a given path - use instead of [TEMPLATE_NAME]',
26
+ ) do |path|
27
+ Template.load_one_from_path(path)
28
+ end
29
+ parser.on('--dashes DASHES', NameTransformation::DASHES_OPTIONS, <<~DESCRIPTION) do |dashes|
30
+ what to do with "-" in PROJECT_NAME when converting to Singer.configuration.project_name_camelcase:
31
+ "modularize": convert to "::", following Ruby convention of mapping dashes to module nesting, or
32
+ "delete": remove entirely, or
33
+ "preserve": leave as-is
34
+ (default is "#{Singer.configuration.dashes}" if `--dashes` isn't given)
35
+ DESCRIPTION
36
+ Singer.configuration.dashes = dashes
37
+ end
38
+ end
23
39
 
24
- encase_name(argvies.pop)
25
- CONFIGURATION.template_name = argvies.shift || DEFAULT_TEMPLATE_NAME
40
+ def self.add_options_listing(parser) # rubocop:disable Metrics/MethodLength
41
+ parser.on('--list-templates', 'list all available templates') do
42
+ list_all_templates
43
+ Kernel.exit
44
+ end
45
+ parser.on('--list-paths', 'list paths used to load files') do
46
+ list_paths
47
+ Kernel.exit
48
+ end
49
+ parser.on('--list-variables', 'list all variables available to templates and in paths') do
50
+ list_variables
51
+ Kernel.exit
52
+ end
26
53
  end
27
54
 
28
55
  def self.list_all_templates
29
- name_column_width = Template.all.keys.map(&:length).max
30
- Template.all.each do |name, template|
31
- puts "#{name.to_s.ljust(name_column_width)} #{template.path}"
32
- end
56
+ columnize_hash(Template.all.transform_values(&:path))
57
+ end
58
+
59
+ def self.list_paths
60
+ columnize_hash(%w[singer_config_dir templates_from_user templates_from_gem].map{ [_1, Paths.send(_1)] })
33
61
  end
34
62
 
35
- def self.encase_name(name)
36
- CONFIGURATION.snakecase_name = name.gsub(/(\w)([A-Z])/){ "#{$1}_#{$2}" }.gsub(/_+/, '_').downcase
37
- CONFIGURATION.camelcase_name = CONFIGURATION.snakecase_name.split('_').map(&:capitalize).join
63
+ NOT_AVAILABLE = '(not available in paths)'.freeze
64
+ def self.list_variables
65
+ columnize_hash(Singer.configuration.members.map do |var|
66
+ [
67
+ var,
68
+ CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS.include?(var.to_s) ? NOT_AVAILABLE : "__#{var.upcase}__",
69
+ ]
70
+ end)
71
+ end
72
+
73
+ def self.columnize_hash(hash_or_array)
74
+ first_column_width = hash_or_array.map{ |k, _v| k.length }.max
75
+ hash_or_array.each do |k, v|
76
+ puts "#{k.to_s.ljust(first_column_width)} #{v}"
77
+ end
38
78
  end
39
79
  end
40
80
  end
data/lib/singer/paths.rb CHANGED
@@ -21,12 +21,16 @@ module Singer
21
21
  File.join(...).gsub(/__([[:alpha:]][[:word:]]+)__/) do |match|
22
22
  potential_config = $1.downcase
23
23
 
24
- if CONFIGURATION.respond_to?(potential_config)
25
- CONFIGURATION.send(potential_config)
24
+ if variable_name_can_be_substituted?(potential_config)
25
+ Singer.configuration.send(potential_config)
26
26
  else
27
27
  match
28
28
  end
29
29
  end
30
30
  end
31
+
32
+ def self.variable_name_can_be_substituted?(name)
33
+ Singer.configuration.respond_to?(name) && !CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS.include?(name)
34
+ end
31
35
  end
32
36
  end
@@ -19,6 +19,11 @@ module Singer
19
19
  end
20
20
  end
21
21
 
22
+ def self.load_one_from_path(path)
23
+ Singer.configuration.template_name = File.basename(path)
24
+ all[Singer.configuration.template_name] = new(path)
25
+ end
26
+
22
27
  attr_reader :path, :files
23
28
 
24
29
  def initialize(path)
@@ -31,9 +36,12 @@ module Singer
31
36
 
32
37
  def generate(output_path)
33
38
  files.each do |file|
34
- source_file = File.join(path, file)
39
+ Singer.configuration.template_file_name_original = file
35
40
  target_file = Paths.output_path(output_path, file)
41
+ Singer.configuration.template_file_name_actual = target_file
42
+
36
43
  FileUtils.mkdir_p(File.dirname(target_file))
44
+ source_file = File.join(path, file)
37
45
  TemplatingMethods.send(:erb, source_file, target_file)
38
46
  end
39
47
  end
@@ -1,3 +1,3 @@
1
1
  module Singer
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
data/lib/singer.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative 'singer/version'
2
2
  require_relative 'singer/configuration'
3
+ require_relative 'singer/name_transformation'
3
4
  require_relative 'singer/option_parsing'
4
5
  require_relative 'singer/paths'
5
6
  require_relative 'singer/template'
@@ -10,14 +11,34 @@ require 'optparse'
10
11
  # Singer, which generates Sinatra apps etc. from templates
11
12
  module Singer
12
13
  class Error < StandardError; end
13
- class NameMissingError < StandardError; end
14
- class TemplateUnknown < StandardError; end
14
+ class NameMissingError < Error; end
15
+ class TemplateUnknown < Error; end
16
+ class ParameterValueUnsupported < Error; end
15
17
 
16
- def self.generate
17
- unless Template.all.key?(CONFIGURATION.template_name)
18
- raise TemplateUnknown, "Template #{CONFIGURATION.template_name.inspect} not found"
19
- end
18
+ def self.configure_and_generate(argvies)
19
+ Singer::OptionParsing.configure(argvies)
20
+ configure_project_name(argvies)
21
+ configure_template(argvies)
22
+ Singer.generate
23
+ end
24
+
25
+ def self.configure_project_name(argvies)
26
+ raise NameMissingError, 'Missing mandatory argument PROJECT_NAME' if argvies.empty?
27
+
28
+ NameTransformation.encase_name(argvies.pop)
29
+ end
20
30
 
21
- Template.all[CONFIGURATION.template_name].generate('.')
31
+ DEFAULT_TEMPLATE_NAME = 'tdd'.freeze
32
+ def self.configure_template(argvies)
33
+ return if configuration.template_name # must have been set via --template-directory
34
+
35
+ configuration.template_name = argvies.pop || DEFAULT_TEMPLATE_NAME
36
+ return if Template.all.key?(configuration.template_name)
37
+
38
+ raise TemplateUnknown, "Template #{configuration.template_name.inspect} not found"
39
+ end
40
+
41
+ def self.generate
42
+ Template.all[configuration.template_name].generate('.')
22
43
  end
23
44
  end
@@ -1,3 +1,3 @@
1
1
  require_relative '../lib/zeitwerk_setup'
2
2
 
3
- run <%= Singer::CONFIGURATION.camelcase_name %>::Server
3
+ run <%= Singer.configuration.project_name_camelcase %>::Server
@@ -1,3 +1,3 @@
1
1
  require 'zeitwerk_setup'
2
2
 
3
- <%= Singer::CONFIGURATION.camelcase_name %>::Server.run!
3
+ <%= Singer.configuration.project_name_camelcase %>::Server.run!
@@ -1,4 +1,4 @@
1
- module <%= Singer::CONFIGURATION.camelcase_name %>
1
+ module <%= Singer.configuration.project_name_camelcase %>
2
2
  # demo class - TODO: remove
3
3
  class Hello
4
4
  def self.greet(target)
@@ -3,7 +3,7 @@ require 'haml'
3
3
  require 'sassc'
4
4
  require 'colorize'
5
5
 
6
- module <%= Singer::CONFIGURATION.camelcase_name %>
6
+ module <%= Singer.configuration.project_name_camelcase %>
7
7
  # Sinatra app with routes
8
8
  class Server < Sinatra::Base
9
9
  set :root, '.' # to make directory structure with views/ work
@@ -4,7 +4,7 @@ class TestServer < Minitest::Test
4
4
  include Rack::Test::Methods
5
5
 
6
6
  def app
7
- <%= Singer::CONFIGURATION.camelcase_name %>::Server
7
+ <%= Singer.configuration.project_name_camelcase %>::Server
8
8
  end
9
9
 
10
10
  def test_top_level_redirects
@@ -10,7 +10,7 @@ class TestServerErrorDumping < Minitest::Test
10
10
  refute message.lines.any?{ _1.include?('/vendor/') } # now you don't
11
11
  end
12
12
 
13
- app = <%= Singer::CONFIGURATION.camelcase_name %>::Server.new! # .new! returns real instance without Sinatra::Wrapper
13
+ app = <%= Singer.configuration.project_name_camelcase %>::Server.new! # .new! returns real instance without Sinatra::Wrapper
14
14
  app.dump_errors!(exc, log)
15
15
  end
16
16
 
@@ -3,6 +3,6 @@ require 'minitest_helper'
3
3
  # TODO: remove
4
4
  class TestHello < Minitest::Test
5
5
  def test_greet_with_a_param_generates_a_greeting_string
6
- assert_equal 'Hello, stranger.', <%= Singer::CONFIGURATION.camelcase_name %>::Hello.greet(:stranger)
6
+ assert_equal 'Hello, stranger.', <%= Singer.configuration.project_name_camelcase %>::Hello.greet(:stranger)
7
7
  end
8
8
  end
@@ -1,7 +1,7 @@
1
1
  !!!
2
2
  %html
3
3
  %head
4
- %title <%= Singer::CONFIGURATION.camelcase_name %>
4
+ %title <%= Singer.configuration.project_name_camelcase %>
5
5
  %meta{charset: 'UTF-8'}
6
6
  %style!= COMPILED_STYLE
7
7
  %body!= yield
@@ -1,13 +1,13 @@
1
1
  require 'colorize'
2
2
  # require 'awesome_print'
3
3
 
4
- class <%= Singer::CONFIGURATION.camelcase_name %>
4
+ class <%= Singer.configuration.project_name_camelcase %>
5
5
  def hello(who:)
6
6
  "Hello, #{who}."
7
7
  end
8
8
  end
9
9
 
10
10
  if File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
11
- ciastka = <%= Singer::CONFIGURATION.camelcase_name %>.new
11
+ ciastka = <%= Singer.configuration.project_name_camelcase %>.new
12
12
  puts "Testing testing: #{ciastka.hello(who: :world).green}"
13
13
  end
@@ -0,0 +1,16 @@
1
+ require 'minitest/rg'
2
+ require 'minitest/autorun' if File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
3
+
4
+ require_relative '../lib/<%= Singer.configuration.project_name_snakecase %>'
5
+
6
+ class Test<%= Singer.configuration.project_name_camelcase %> < Minitest::Test
7
+ # TEST_INPUT = File.read('input_mini.txt').lines(chomp: true)
8
+
9
+ def setup
10
+ @<%= Singer.configuration.project_name_snakecase %> = <%= Singer.configuration.project_name_camelcase %>.new
11
+ end
12
+
13
+ def test_hello_interpolates_into_string
14
+ assert_equal 'Hello, world.', @<%= Singer.configuration.project_name_snakecase %>.hello(who: :world)
15
+ end
16
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: singer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Chludziński
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: erb
@@ -38,6 +38,7 @@ files:
38
38
  - exe/singer
39
39
  - lib/singer.rb
40
40
  - lib/singer/configuration.rb
41
+ - lib/singer/name_transformation.rb
41
42
  - lib/singer/option_parsing.rb
42
43
  - lib/singer/paths.rb
43
44
  - lib/singer/template.rb
@@ -48,8 +49,8 @@ files:
48
49
  - templates/sinatra_app/config/config.ru
49
50
  - templates/sinatra_app/config/puma.rb
50
51
  - templates/sinatra_app/exe/server.rb
51
- - templates/sinatra_app/lib/__SNAKECASE_NAME__/hello.rb
52
- - templates/sinatra_app/lib/__SNAKECASE_NAME__/server.rb
52
+ - templates/sinatra_app/lib/__PROJECT_NAME_SNAKECASE__/hello.rb
53
+ - templates/sinatra_app/lib/__PROJECT_NAME_SNAKECASE__/server.rb
53
54
  - templates/sinatra_app/lib/zeitwerk_setup.rb
54
55
  - templates/sinatra_app/test/minitest_helper.rb
55
56
  - templates/sinatra_app/test/server/test_server.rb
@@ -60,8 +61,8 @@ files:
60
61
  - templates/sinatra_app/views/layout.haml
61
62
  - templates/sinatra_app/views/style.sass
62
63
  - templates/tdd/Gemfile
63
- - templates/tdd/lib/__SNAKECASE_NAME__.rb
64
- - templates/tdd/test/test___SNAKECASE_NAME__.rb
64
+ - templates/tdd/lib/__PROJECT_NAME_SNAKECASE__.rb
65
+ - templates/tdd/test/test___PROJECT_NAME_SNAKECASE__.rb
65
66
  homepage: https://gitlab.com/tanstaafl/singer/
66
67
  licenses:
67
68
  - MIT
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  - !ruby/object:Gem::Version
84
85
  version: '0'
85
86
  requirements: []
86
- rubygems_version: 3.6.2
87
+ rubygems_version: 3.6.7
87
88
  specification_version: 4
88
89
  summary: Singer generates code (such as Sinatra apps) from templates.
89
90
  test_files: []
@@ -1,16 +0,0 @@
1
- require 'minitest/rg'
2
- require 'minitest/autorun' if $PROGRAM_NAME == __FILE__
3
-
4
- require_relative '../lib/<%= Singer::CONFIGURATION.snakecase_name %>'
5
-
6
- class Test<%= Singer::CONFIGURATION.camelcase_name %> < Minitest::Test
7
- # TEST_INPUT = File.read('input_mini.txt').lines(chomp: true)
8
-
9
- def setup
10
- @<%= Singer::CONFIGURATION.snakecase_name %> = <%= Singer::CONFIGURATION.camelcase_name %>.new
11
- end
12
-
13
- def test_hello_interpolates_into_string
14
- assert_equal 'Hello, world.', @<%= Singer::CONFIGURATION.snakecase_name %>.hello(who: :world)
15
- end
16
- end