singer 1.1.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: cd871af4e10b86d0362c5bef72223344e86d5f81cb6a2871e901ec588f2d97b8
4
- data.tar.gz: 5bbab829554eee4d7da41e43f07fec57784269906da6a7f866a466a73f1a9e92
3
+ metadata.gz: b91934a916b675cfddecdac26f48673a77e9e2f014ae94a7db7623df0adaac67
4
+ data.tar.gz: f8a8384bdbba9f096d303e91fbd68a2b1063239d89e5f072d1e8e551cee1b7d2
5
5
  SHA512:
6
- metadata.gz: ba2fecb7f5b4a230e5eb26d7e8fef5566e1753fec82952b431779b7720fc3c525beb26a9e9e977c2d328648b11531e1bc4e760c1edbde219f1f025abb0be72d9
7
- data.tar.gz: 3f121f9b2383ef48b822e1dac292e5a9f8455ded6cb9816b4fcd79b8139e64d46709c5fd2f9473cb200a1f8fc4808586e16bcfad574d91c86c8892bb67aba96a
6
+ metadata.gz: 5a0d34e3e4ba0b18a55151f796f30d8b0bad715f74a084f37456a4cdebb74d8f66543db90003dc84877a19ef295f0664f1e5ba190cd137d67a670f8819802d01
7
+ data.tar.gz: d9245c0f2fcbc5671a078daf5bcf71977ba2ec661c1b599e0aedaa2e679948bc4d9ebad738a6302daefe515dae253a5d8d59d6b12b0779b7796b6c9bf52b35d2
data/README.md CHANGED
@@ -28,9 +28,9 @@ Each template resides in a subdirectory - the subdirectory's name becomes the te
28
28
 
29
29
  ### Variables available to templates
30
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:
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
32
  ```ruby
33
- class <%= Singer::CONFIGURATION.project_name_camelcase %>
33
+ class <%= Singer.configuration.project_name_camelcase %>
34
34
  ```
35
35
  will, when Singer is called with `singer my_template foo_bar_baz`, result in this written to output file:
36
36
  ```ruby
@@ -39,6 +39,16 @@ class FooBarBaz
39
39
 
40
40
  To see an up-to-date list of available variables, run `singer --list-variables`.
41
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
+
42
52
  ### Special segments in template paths
43
53
 
44
54
  Any of the variables above can be also used in paths/filenames of template files, capitalized and surrounded by double underscores.
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,9 +1,16 @@
1
+ # main module, has `.configuration` reader
1
2
  module Singer
2
3
  # holds settings
3
- CONFIGURATION = Struct.new(
4
+ Configuration = Struct.new(
5
+ 'Configuration',
4
6
  :template_name,
5
7
  :project_name_original, :project_name_camelcase, :project_name_snakecase,
6
8
  :template_file_name_original, :template_file_name_actual,
7
- ).new
9
+ :dashes,
10
+ )
8
11
  CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS = %w[template_file_name_actual].freeze
12
+
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
15
+ end
9
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,61 +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
- def self.configure(argvies) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
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
20
- parser.on('--show-paths', 'show paths used to load files') do
21
- show_paths
22
- Kernel.exit
23
- end
24
- parser.on('--list-variables', 'list all variables available to templates') do
25
- list_variables
26
- Kernel.exit
27
- end
18
+ add_options_functional(parser)
19
+ add_options_listing(parser)
28
20
  end.parse!(argvies, into: options)
21
+ end
29
22
 
30
- 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
31
39
 
32
- encase_name(argvies.pop)
33
- 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
34
53
  end
35
54
 
36
55
  def self.list_all_templates
37
56
  columnize_hash(Template.all.transform_values(&:path))
38
57
  end
39
58
 
40
- def self.show_paths
41
- columnize_hash(%w[singer_config_dir templates_from_user templates_from_gem].to_h{ [_1, Paths.send(_1)] })
42
- end
43
-
44
- def self.columnize_hash(hash)
45
- first_column_width = hash.keys.map(&:length).max
46
- hash.each do |k, v|
47
- puts "#{k.to_s.ljust(first_column_width)} #{v}"
48
- end
59
+ def self.list_paths
60
+ columnize_hash(%w[singer_config_dir templates_from_user templates_from_gem].map{ [_1, Paths.send(_1)] })
49
61
  end
50
62
 
63
+ NOT_AVAILABLE = '(not available in paths)'.freeze
51
64
  def self.list_variables
52
- puts CONFIGURATION.members
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)
53
71
  end
54
72
 
55
- def self.encase_name(name)
56
- CONFIGURATION.project_name_original = name
57
- CONFIGURATION.project_name_snakecase = name.gsub(/(\w)([A-Z])/){ "#{$1}_#{$2}" }.gsub(/_+/, '_').downcase
58
- CONFIGURATION.project_name_camelcase = CONFIGURATION.project_name_snakecase.split('_').map(&:capitalize).join
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
59
78
  end
60
79
  end
61
80
  end
data/lib/singer/paths.rb CHANGED
@@ -22,7 +22,7 @@ module Singer
22
22
  potential_config = $1.downcase
23
23
 
24
24
  if variable_name_can_be_substituted?(potential_config)
25
- CONFIGURATION.send(potential_config)
25
+ Singer.configuration.send(potential_config)
26
26
  else
27
27
  match
28
28
  end
@@ -30,7 +30,7 @@ module Singer
30
30
  end
31
31
 
32
32
  def self.variable_name_can_be_substituted?(name)
33
- CONFIGURATION.respond_to?(name) && !CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS.include?(name)
33
+ Singer.configuration.respond_to?(name) && !CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS.include?(name)
34
34
  end
35
35
  end
36
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,9 @@ module Singer
31
36
 
32
37
  def generate(output_path)
33
38
  files.each do |file|
34
- CONFIGURATION.template_file_name_original = file
39
+ Singer.configuration.template_file_name_original = file
35
40
  target_file = Paths.output_path(output_path, file)
36
- CONFIGURATION.template_file_name_actual = target_file
41
+ Singer.configuration.template_file_name_actual = target_file
37
42
 
38
43
  FileUtils.mkdir_p(File.dirname(target_file))
39
44
  source_file = File.join(path, file)
@@ -1,3 +1,3 @@
1
1
  module Singer
2
- VERSION = '1.1.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.project_name_camelcase %>::Server
3
+ run <%= Singer.configuration.project_name_camelcase %>::Server
@@ -1,3 +1,3 @@
1
1
  require 'zeitwerk_setup'
2
2
 
3
- <%= Singer::CONFIGURATION.project_name_camelcase %>::Server.run!
3
+ <%= Singer.configuration.project_name_camelcase %>::Server.run!
@@ -1,4 +1,4 @@
1
- module <%= Singer::CONFIGURATION.project_name_camelcase %>
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.project_name_camelcase %>
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.project_name_camelcase %>::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.project_name_camelcase %>::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.project_name_camelcase %>::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.project_name_camelcase %>
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.project_name_camelcase %>
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.project_name_camelcase %>.new
11
+ ciastka = <%= Singer.configuration.project_name_camelcase %>.new
12
12
  puts "Testing testing: #{ciastka.hello(who: :world).green}"
13
13
  end
@@ -1,16 +1,16 @@
1
1
  require 'minitest/rg'
2
- require 'minitest/autorun' if $PROGRAM_NAME == __FILE__
2
+ require 'minitest/autorun' if File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
3
3
 
4
- require_relative '../lib/<%= Singer::CONFIGURATION.project_name_snakecase %>'
4
+ require_relative '../lib/<%= Singer.configuration.project_name_snakecase %>'
5
5
 
6
- class Test<%= Singer::CONFIGURATION.project_name_camelcase %> < Minitest::Test
6
+ class Test<%= Singer.configuration.project_name_camelcase %> < Minitest::Test
7
7
  # TEST_INPUT = File.read('input_mini.txt').lines(chomp: true)
8
8
 
9
9
  def setup
10
- @<%= Singer::CONFIGURATION.project_name_snakecase %> = <%= Singer::CONFIGURATION.project_name_camelcase %>.new
10
+ @<%= Singer.configuration.project_name_snakecase %> = <%= Singer.configuration.project_name_camelcase %>.new
11
11
  end
12
12
 
13
13
  def test_hello_interpolates_into_string
14
- assert_equal 'Hello, world.', @<%= Singer::CONFIGURATION.project_name_snakecase %>.hello(who: :world)
14
+ assert_equal 'Hello, world.', @<%= Singer.configuration.project_name_snakecase %>.hello(who: :world)
15
15
  end
16
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.1.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-02-09 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
@@ -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: []