singer 0.4.0 → 1.1.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: 23ff6172f3189c234f2b95c1aaee2a67e5b91ffd10c88375c54e37cb4417abb5
4
- data.tar.gz: 037ff1bd0e545efd4156ad9ff84cb2934070f0e704e05156acbff1fd7d787cfb
3
+ metadata.gz: cd871af4e10b86d0362c5bef72223344e86d5f81cb6a2871e901ec588f2d97b8
4
+ data.tar.gz: 5bbab829554eee4d7da41e43f07fec57784269906da6a7f866a466a73f1a9e92
5
5
  SHA512:
6
- metadata.gz: 494f03f90b0584d1c0ba3ae4b72454151e8379591082e51fa61cbaae3ad65c50b91463ef52ba56208a7ef6f63d6c8b75f4c4f3c99430a9b367284e337f77cf59
7
- data.tar.gz: 55646990590d66996705547ed95ff21b21a6a19609db53231c6f416d91fc37d84f0d1fc1c12189b932ade10a479ebef2f619cd5e1a0c8d3b07eb8a1f58f89d4a
6
+ metadata.gz: ba2fecb7f5b4a230e5eb26d7e8fef5566e1753fec82952b431779b7720fc3c525beb26a9e9e977c2d328648b11531e1bc4e760c1edbde219f1f025abb0be72d9
7
+ data.tar.gz: 3f121f9b2383ef48b822e1dac292e5a9f8455ded6cb9816b4fcd79b8139e64d46709c5fd2f9473cb200a1f8fc4808586e16bcfad574d91c86c8892bb67aba96a
data/README.md CHANGED
@@ -8,7 +8,51 @@ Run `gem install singer`.
8
8
 
9
9
  ## Usage
10
10
 
11
- TODO: Write usage instructions here
11
+ ```bash
12
+ singer [OPTION]... [TEMPLATE_NAME] PROJECT_NAME
13
+ ```
14
+
15
+ Run `singer --help` to see up-to-date list of options.
16
+ Run `singer --list-templates` to see available templates' names, both shipped with the gem and found on current system.
17
+
18
+ For `PROJECT_NAME`, using `snake_case` is best, although an attempt will be made to understand `CamelCase`, too.
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
+ ### Special segments in template paths
43
+
44
+ Any of the variables above can be also used in paths/filenames of template files, capitalized and surrounded by double underscores.
45
+ So, if `my_template` has a file at this path:
46
+ ```
47
+ docs_for___PROJECT_NAME_SNAKECASE__/__PROJECT_NAME_CAMELCASE__ - humble beginnings.txt
48
+ ```
49
+ and you run `singer my_template alpha_beta`, a file named like this will be created:
50
+ ```
51
+ docs_for_alpha_beta/AlphaBeta - humble beginnings.txt
52
+ ```
53
+
54
+ 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.
55
+ "Stack level too deep" and all that ;)
12
56
 
13
57
  ## Development
14
58
 
data/exe/singer CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  require 'singer'
4
4
 
5
- Singer.configure(ARGV)
5
+ Singer::OptionParsing.configure(ARGV)
6
6
  Singer.generate
@@ -1,4 +1,9 @@
1
1
  module Singer
2
2
  # holds settings
3
- CONFIGURATION = Struct.new(:template_name, :camelcase_name, :snakecase_name).new
3
+ CONFIGURATION = Struct.new(
4
+ :template_name,
5
+ :project_name_original, :project_name_camelcase, :project_name_snakecase,
6
+ :template_file_name_original, :template_file_name_actual,
7
+ ).new
8
+ CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS = %w[template_file_name_actual].freeze
4
9
  end
@@ -0,0 +1,61 @@
1
+ module Singer
2
+ # extracts what we need from ARGV
3
+ class OptionParsing
4
+ DEFAULT_TEMPLATE_NAME = 'tdd'.freeze
5
+
6
+ def self.configure(argvies) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
7
+ options = {}
8
+ OptionParser.new do |parser|
9
+ parser.banner = <<~USAGE
10
+ Usage: #{$PROGRAM_NAME} [OPTION]... [TEMPLATE_NAME] PROJECT_NAME
11
+ Generates a project from a multi-file template.
12
+ PROJECT_NAME can be in snake_case or CamelCase.
13
+
14
+ Options:
15
+ 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
28
+ end.parse!(argvies, into: options)
29
+
30
+ raise NameMissingError, 'Missing mandatory argument PROJECT_NAME' if argvies.empty?
31
+
32
+ encase_name(argvies.pop)
33
+ CONFIGURATION.template_name = argvies.shift || DEFAULT_TEMPLATE_NAME
34
+ end
35
+
36
+ def self.list_all_templates
37
+ columnize_hash(Template.all.transform_values(&:path))
38
+ end
39
+
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
49
+ end
50
+
51
+ def self.list_variables
52
+ puts CONFIGURATION.members
53
+ end
54
+
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
59
+ end
60
+ end
61
+ end
data/lib/singer/paths.rb CHANGED
@@ -1,6 +1,18 @@
1
1
  module Singer
2
2
  # helps find files
3
3
  class Paths
4
+ def self.xdg_config_dir
5
+ @xdg_config_dir ||= ENV.fetch('XDG_CONFIG_DIR', File.join(Dir.home, '.config'))
6
+ end
7
+
8
+ def self.singer_config_dir
9
+ @singer_config_dir ||= File.join(xdg_config_dir, 'singer')
10
+ end
11
+
12
+ def self.templates_from_user
13
+ @templates_from_user ||= File.join(singer_config_dir, 'templates')
14
+ end
15
+
4
16
  def self.templates_from_gem
5
17
  @templates_from_gem ||= File.expand_path('../../templates', __dir__)
6
18
  end
@@ -9,12 +21,16 @@ module Singer
9
21
  File.join(...).gsub(/__([[:alpha:]][[:word:]]+)__/) do |match|
10
22
  potential_config = $1.downcase
11
23
 
12
- if CONFIGURATION.respond_to?(potential_config)
24
+ if variable_name_can_be_substituted?(potential_config)
13
25
  CONFIGURATION.send(potential_config)
14
26
  else
15
27
  match
16
28
  end
17
29
  end
18
30
  end
31
+
32
+ def self.variable_name_can_be_substituted?(name)
33
+ CONFIGURATION.respond_to?(name) && !CONFIGURATION_VARIABLES_FORBIDDEN_IN_PATHS.include?(name)
34
+ end
19
35
  end
20
36
  end
@@ -8,7 +8,13 @@ module Singer
8
8
  end
9
9
 
10
10
  def self.load_all
11
- Dir[File.join(Paths.templates_from_gem, '*')].to_h do |template_dir|
11
+ [Paths.templates_from_gem, Paths.templates_from_user]
12
+ .map{ load_from_directory(_1) }
13
+ .reduce(&:merge)
14
+ end
15
+
16
+ def self.load_from_directory(dir)
17
+ Dir[File.join(dir, '*')].to_h do |template_dir|
12
18
  [File.basename(template_dir), new(template_dir)]
13
19
  end
14
20
  end
@@ -25,9 +31,12 @@ module Singer
25
31
 
26
32
  def generate(output_path)
27
33
  files.each do |file|
28
- source_file = File.join(path, file)
34
+ CONFIGURATION.template_file_name_original = file
29
35
  target_file = Paths.output_path(output_path, file)
36
+ CONFIGURATION.template_file_name_actual = target_file
37
+
30
38
  FileUtils.mkdir_p(File.dirname(target_file))
39
+ source_file = File.join(path, file)
31
40
  TemplatingMethods.send(:erb, source_file, target_file)
32
41
  end
33
42
  end
@@ -1,3 +1,3 @@
1
1
  module Singer
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
data/lib/singer.rb CHANGED
@@ -1,29 +1,18 @@
1
1
  require_relative 'singer/version'
2
2
  require_relative 'singer/configuration'
3
+ require_relative 'singer/option_parsing'
3
4
  require_relative 'singer/paths'
4
5
  require_relative 'singer/template'
5
6
  require_relative 'singer/templating_methods'
6
7
 
8
+ require 'optparse'
9
+
7
10
  # Singer, which generates Sinatra apps etc. from templates
8
11
  module Singer
9
12
  class Error < StandardError; end
10
13
  class NameMissingError < StandardError; end
11
14
  class TemplateUnknown < StandardError; end
12
15
 
13
- DEFAULT_TEMPLATE_NAME = 'tdd'.freeze
14
-
15
- def self.configure(argvies)
16
- raise NameMissingError, 'Missing mandatory argument NAME' if argvies.empty?
17
-
18
- encase_name(argvies.pop)
19
- CONFIGURATION.template_name = argvies.shift || DEFAULT_TEMPLATE_NAME
20
- end
21
-
22
- def self.encase_name(name)
23
- CONFIGURATION.snakecase_name = name.gsub(/(\w)([A-Z])/){ "#{$1}_#{$2}" }.gsub(/_+/, '_').downcase
24
- CONFIGURATION.camelcase_name = CONFIGURATION.snakecase_name.split('_').map(&:capitalize).join
25
- end
26
-
27
16
  def self.generate
28
17
  unless Template.all.key?(CONFIGURATION.template_name)
29
18
  raise TemplateUnknown, "Template #{CONFIGURATION.template_name.inspect} not found"
@@ -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 $PROGRAM_NAME == __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: 0.4.0
4
+ version: 1.1.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-26 00:00:00.000000000 Z
10
+ date: 2025-02-09 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/option_parsing.rb
41
42
  - lib/singer/paths.rb
42
43
  - lib/singer/template.rb
43
44
  - lib/singer/templating_methods.rb
@@ -47,8 +48,8 @@ files:
47
48
  - templates/sinatra_app/config/config.ru
48
49
  - templates/sinatra_app/config/puma.rb
49
50
  - templates/sinatra_app/exe/server.rb
50
- - templates/sinatra_app/lib/__SNAKECASE_NAME__/hello.rb
51
- - templates/sinatra_app/lib/__SNAKECASE_NAME__/server.rb
51
+ - templates/sinatra_app/lib/__PROJECT_NAME_SNAKECASE__/hello.rb
52
+ - templates/sinatra_app/lib/__PROJECT_NAME_SNAKECASE__/server.rb
52
53
  - templates/sinatra_app/lib/zeitwerk_setup.rb
53
54
  - templates/sinatra_app/test/minitest_helper.rb
54
55
  - templates/sinatra_app/test/server/test_server.rb
@@ -59,8 +60,8 @@ files:
59
60
  - templates/sinatra_app/views/layout.haml
60
61
  - templates/sinatra_app/views/style.sass
61
62
  - templates/tdd/Gemfile
62
- - templates/tdd/lib/__SNAKECASE_NAME__.rb
63
- - templates/tdd/test/test___SNAKECASE_NAME__.rb
63
+ - templates/tdd/lib/__PROJECT_NAME_SNAKECASE__.rb
64
+ - templates/tdd/test/test___PROJECT_NAME_SNAKECASE__.rb
64
65
  homepage: https://gitlab.com/tanstaafl/singer/
65
66
  licenses:
66
67
  - MIT
@@ -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