singer 0.3.0 → 1.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: d324ad6ec097b02fe9db6ab844205b37dd6a53450d69d5c7780d5bd7e7743efa
4
- data.tar.gz: 9990fccac83a6ce04a3d89da50e7dd6b109edbfb74fa48e938b40839c34dfe5e
3
+ metadata.gz: 8d116f657ac1b8d3b57e540ae37bf0d74c21fe357938fbb6aa33309663b9add5
4
+ data.tar.gz: 7854c981adfd45a2989647f6302022ff0b40e66b47960c2fc536cd428dc99c93
5
5
  SHA512:
6
- metadata.gz: b5ae6fa28d28668e5afa00615770a6e69628f767e7adaa1344d26bc6e9e0e4c26a60c17aeb51ccae165ad4e50a77fac52b9e38d391d3de01ff398df36bda6682
7
- data.tar.gz: f1f56ff0b4234f45d9a68835d9c69110481567aa7a3554f06240913b6125e9017439d601754e522710b326bf6f402b1771f181b2ffb27cf746da1a8fc3762990
6
+ metadata.gz: 933cc4b306f621d57598f0b1d5f72993fd6c25afea2496aa89f088847b42374b8a01bf68d709a2c3f154455b02b6158f2072677ec23b55ff1b359020c24cfbe5
7
+ data.tar.gz: 0ed47b046d03641227f28faa499d9ea50cd80901ddf7efa75f5c1da4df07a54b5ad435bb338158047438175862c17b310e92d19ee94ecfafdf135d9810dce468
data/README.md CHANGED
@@ -8,7 +8,14 @@ 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.
12
19
 
13
20
  ## Development
14
21
 
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,4 @@
1
1
  module Singer
2
2
  # holds settings
3
- CONFIGURATION = Struct.new(:camelcase_name, :snakecase_name).new
3
+ CONFIGURATION = Struct.new(:template_name, :camelcase_name, :snakecase_name).new
4
4
  end
@@ -0,0 +1,40 @@
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/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
+ end.parse!(argvies, into: options)
21
+
22
+ raise NameMissingError, 'Missing mandatory argument PROJECT_NAME' if argvies.empty?
23
+
24
+ encase_name(argvies.pop)
25
+ CONFIGURATION.template_name = argvies.shift || DEFAULT_TEMPLATE_NAME
26
+ end
27
+
28
+ 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
33
+ end
34
+
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
38
+ end
39
+ end
40
+ 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
@@ -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
@@ -18,7 +24,7 @@ module Singer
18
24
  def initialize(path)
19
25
  @path = path
20
26
  Dir.chdir(path) do # we could use only Dir[base: path], but all the code uses relative paths
21
- entries = Dir['**/*']
27
+ entries = Dir.glob('**/*', flags: File::FNM_DOTMATCH)
22
28
  @files = entries.select{ File.file?(_1) }
23
29
  end
24
30
  end
@@ -1,3 +1,3 @@
1
1
  module Singer
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
data/lib/singer.rb CHANGED
@@ -1,26 +1,23 @@
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
-
12
- def self.configure(argvies)
13
- raise NameMissingError, 'Missing mandatory argument NAME' if argvies.empty?
14
-
15
- encase_name(argvies.last)
16
- end
17
-
18
- def self.encase_name(name)
19
- CONFIGURATION.snakecase_name = name.gsub(/(\w)([A-Z])/){ "#{$1}_#{$2}" }.gsub(/_+/, '_').downcase
20
- CONFIGURATION.camelcase_name = CONFIGURATION.snakecase_name.split('_').map(&:capitalize).join
21
- end
14
+ class TemplateUnknown < StandardError; end
22
15
 
23
16
  def self.generate
24
- Template.all['tdd'].generate('.')
17
+ unless Template.all.key?(CONFIGURATION.template_name)
18
+ raise TemplateUnknown, "Template #{CONFIGURATION.template_name.inspect} not found"
19
+ end
20
+
21
+ Template.all[CONFIGURATION.template_name].generate('.')
25
22
  end
26
23
  end
@@ -0,0 +1,12 @@
1
+ # Ruby stuff
2
+ /coverage/
3
+ /doc/
4
+ /.bundle/
5
+ /vendor/
6
+
7
+ # prototyping and temporary stuff
8
+ /tmp/
9
+ /todo/
10
+
11
+ # database
12
+ /db/*.sqlite3
@@ -0,0 +1,28 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'colorize'
4
+ gem 'haml'
5
+ gem 'puma'
6
+ gem 'rackup'
7
+ gem 'sassc'
8
+ gem 'sinatra'
9
+ gem 'zeitwerk'
10
+
11
+ group :test do # for humans
12
+ gem 'fivemat'
13
+ gem 'minitest-rg'
14
+ end
15
+
16
+ group :test, :ci do
17
+ gem 'minitest', '~> 5.23.0' # fivemat double-outputs with newer than this
18
+ gem 'mocha'
19
+ gem 'rack-test'
20
+ gem 'rake'
21
+ gem 'rubocop'
22
+ gem 'simplecov'
23
+ end
24
+
25
+ group :ci do # for CI automation
26
+ gem 'minitest-junit'
27
+ gem 'simplecov-cobertura'
28
+ end
@@ -0,0 +1,3 @@
1
+ require_relative '../lib/zeitwerk_setup'
2
+
3
+ run <%= Singer::CONFIGURATION.camelcase_name %>::Server
@@ -0,0 +1,6 @@
1
+ port 80, '0.0.0.0'
2
+
3
+ app_domain = ENV.fetch('APP_DOMAIN', nil)
4
+ cert = "/etc/letsencrypt/live/#{app_domain}/fullchain.pem"
5
+ key = "/etc/letsencrypt/live/#{app_domain}/privkey.pem"
6
+ ssl_bind '0.0.0.0', 443, {cert:, key:} if File.exist?(cert) && File.exist?(key)
@@ -0,0 +1,3 @@
1
+ require 'zeitwerk_setup'
2
+
3
+ <%= Singer::CONFIGURATION.camelcase_name %>::Server.run!
@@ -0,0 +1,8 @@
1
+ module <%= Singer::CONFIGURATION.camelcase_name %>
2
+ # demo class - TODO: remove
3
+ class Hello
4
+ def self.greet(target)
5
+ "Hello, #{target}."
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ require 'sinatra/base'
2
+ require 'haml'
3
+ require 'sassc'
4
+ require 'colorize'
5
+
6
+ module <%= Singer::CONFIGURATION.camelcase_name %>
7
+ # Sinatra app with routes
8
+ class Server < Sinatra::Base
9
+ set :root, '.' # to make directory structure with views/ work
10
+ enable :logging
11
+ configure :development do
12
+ set :bind, '0.0.0.0'
13
+ set :port, 80
14
+ end
15
+
16
+ def dump_errors!(boom, out = @env['rack.errors'])
17
+ filtered_backtrace = boom.backtrace.reject{ _1.include?('/vendor/') }
18
+ header = "#{Time.now.strftime('%F %T')} - #{boom.class} - #{boom.message.white.on_red}:"
19
+ error_message = [header, *filtered_backtrace].join("\n\t")
20
+ out.puts(error_message)
21
+ end
22
+
23
+ def self.compiled_style
24
+ SassC::Engine.new(File.read('views/style.sass'), syntax: :sass).render
25
+ end
26
+ COMPILED_STYLE = compiled_style
27
+
28
+ get '/' do
29
+ redirect '/hello/world'
30
+ end
31
+
32
+ get '/hello/:target' do # TODO: remove
33
+ @salutation = Hello.greet params[:target]
34
+
35
+ haml :index
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ require 'zeitwerk'
2
+
3
+ loader = Zeitwerk::Loader.new
4
+ loader.push_dir 'lib'
5
+ loader.ignore('lib/zeitwerk_setup.rb')
6
+ loader.setup
@@ -0,0 +1,18 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/rg'
3
+
4
+ require 'mocha/minitest'
5
+ Mocha.configure do |c|
6
+ c.display_matching_invocations_on_failure = true
7
+ c.strict_keyword_argument_matching = true
8
+ c.stubbing_method_on_non_mock_object = :allow
9
+ c.stubbing_method_unnecessarily = :prevent
10
+ c.stubbing_non_existent_method = :prevent
11
+ c.stubbing_non_public_method = :allow
12
+ end
13
+
14
+ require 'rack/test'
15
+
16
+ require 'zeitwerk_setup'
17
+
18
+ ENV['APP_ENV'] = 'test' # so that Sinatra and Sequel know we're in test mode
@@ -0,0 +1,32 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestServer < Minitest::Test
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ <%= Singer::CONFIGURATION.camelcase_name %>::Server
8
+ end
9
+
10
+ def test_top_level_redirects
11
+ get '/'
12
+
13
+ assert last_response.redirect?
14
+ assert_match %r{/hello/world$}, last_response.headers['location']
15
+ end
16
+
17
+ # TODO: remove
18
+ def test_hello_with_param_slug_includes_a_greeting
19
+ get '/hello/everyone'
20
+
21
+ refute_predicate last_response, :redirect?
22
+ assert_predicate last_response, :ok?
23
+ assert_includes last_response.body, 'Hello, everyone.'
24
+ end
25
+
26
+ def test_hello_without_param_is_a_404_error
27
+ get '/hello'
28
+
29
+ refute_predicate last_response, :ok?
30
+ assert_equal 404, last_response.status
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestServerErrorDumping < Minitest::Test
4
+ def test_dump_errors_backtrace_is_filtered_against_vendor
5
+ exc = generate_exception_with_realistic_backtrace
6
+ assert exc.backtrace.any?{ _1.include?('/vendor/') } # now you see me
7
+
8
+ log = mock()
9
+ log.expects(:puts).with do |message|
10
+ refute message.lines.any?{ _1.include?('/vendor/') } # now you don't
11
+ end
12
+
13
+ app = <%= Singer::CONFIGURATION.camelcase_name %>::Server.new! # .new! returns real instance without Sinatra::Wrapper
14
+ app.dump_errors!(exc, log)
15
+ end
16
+
17
+ def generate_exception_with_realistic_backtrace
18
+ raise 'ciastka'
19
+ rescue RuntimeError => exc
20
+ exc
21
+ end
22
+
23
+ def test_generate_exception_with_realistic_backtrace
24
+ assert_kind_of RuntimeError, generate_exception_with_realistic_backtrace
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestAutoloading < Minitest::Test
4
+ def test_zeitwerk_has_one_loader
5
+ refute_empty Zeitwerk::Registry.loaders
6
+ assert_equal 1, Zeitwerk::Registry.loaders.size
7
+ end
8
+
9
+ # this test is kinda slow, but not as slow as Rack::Test
10
+ def test_zeitwerk_eager_autoloading_works
11
+ loader = Zeitwerk::Registry.loaders.first
12
+ # loader.log!
13
+ loader.eager_load(force: true)
14
+ rescue Zeitwerk::NameError => exc
15
+ flunk exc.message
16
+ else
17
+ pass
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ require 'minitest_helper'
2
+
3
+ # TODO: remove
4
+ class TestHello < Minitest::Test
5
+ def test_greet_with_a_param_generates_a_greeting_string
6
+ assert_equal 'Hello, stranger.', <%= Singer::CONFIGURATION.camelcase_name %>::Hello.greet(:stranger)
7
+ end
8
+ end
@@ -0,0 +1 @@
1
+ #salutation= @salutation
@@ -0,0 +1,7 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title <%= Singer::CONFIGURATION.camelcase_name %>
5
+ %meta{charset: 'UTF-8'}
6
+ %style!= COMPILED_STYLE
7
+ %body!= yield
@@ -0,0 +1,17 @@
1
+ html
2
+ --primary-font: Departure Mono, Joystix Monospace, ui-monospace, monospace, sans-serif
3
+ font-family: var(--primary-font)
4
+ font-size: 64px
5
+ body
6
+ --color-text: Black
7
+ --color-background: White
8
+ color: var(--color-text)
9
+ background-color: var(--color-background)
10
+ margin: 0px
11
+ body > *
12
+ padding: 1em
13
+
14
+ @media (prefers-color-scheme: dark)
15
+ body
16
+ --color-text: White
17
+ --color-background: #222
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # gem 'awesome_print'
4
+ gem 'colorize'
5
+ gem 'minitest'
6
+ gem 'minitest-rg'
7
+ # gem 'rake'
8
+ # gem 'rubocop'
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.3.0
4
+ version: 1.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-21 00:00:00.000000000 Z
10
+ date: 2025-01-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: erb
@@ -38,10 +38,28 @@ 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
44
45
  - lib/singer/version.rb
46
+ - templates/sinatra_app/.gitignore
47
+ - templates/sinatra_app/Gemfile
48
+ - templates/sinatra_app/config/config.ru
49
+ - templates/sinatra_app/config/puma.rb
50
+ - templates/sinatra_app/exe/server.rb
51
+ - templates/sinatra_app/lib/__SNAKECASE_NAME__/hello.rb
52
+ - templates/sinatra_app/lib/__SNAKECASE_NAME__/server.rb
53
+ - templates/sinatra_app/lib/zeitwerk_setup.rb
54
+ - templates/sinatra_app/test/minitest_helper.rb
55
+ - templates/sinatra_app/test/server/test_server.rb
56
+ - templates/sinatra_app/test/server/test_server_error_dumping.rb
57
+ - templates/sinatra_app/test/test_autoloading.rb
58
+ - templates/sinatra_app/test/test_hello.rb
59
+ - templates/sinatra_app/views/index.haml
60
+ - templates/sinatra_app/views/layout.haml
61
+ - templates/sinatra_app/views/style.sass
62
+ - templates/tdd/Gemfile
45
63
  - templates/tdd/lib/__SNAKECASE_NAME__.rb
46
64
  - templates/tdd/test/test___SNAKECASE_NAME__.rb
47
65
  homepage: https://gitlab.com/tanstaafl/singer/