rbytes 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2018b6cc0392420457e2edd1a8ec027f55978148bbb4ea5fba8e517116dc166
4
- data.tar.gz: de96f8614e325160cda52825af368626ba6f70769dc1323efab289a5023c47d0
3
+ metadata.gz: 53595c57bee23eca0389d923a2b7fec3c67e570c07a4405cc705afe658081a3a
4
+ data.tar.gz: d96f2d9536487f2432a0f56c9d9407c6caa7f04a2d9e98ecea72111e3e88e9b0
5
5
  SHA512:
6
- metadata.gz: 0b1021dc75c1ae9c56732c385101d53e65f2d8500f72b37e001e685db01a4b713ec6cd9052c0d0959a7acde61902c52c487f314c580be600aec2153f60957d4b
7
- data.tar.gz: 84e337dcbb359ac314731982e0ae8ebcb0dd1e879df4eba8fc05699a47b7bb0fef83d3485b320d9c88aa5370d0d1862bfde59bdaa9b4e3e3f77f6565aa7b6f18
6
+ metadata.gz: 9bee2dcc4ecf593b7aa7d7289955eb4518dadf74087bafa523880647e193bd2b8d16af2542c27e73f2a6d05fcbd2a73ea5de2fef02bfdd202dc013d4024c147a
7
+ data.tar.gz: 8f5928ca9ba174fab8e830825f2bad113fbb295cbd84b106cd0364d0519037750c1343acc19b40f1d1152f7ec1d92760180d594730247ff4c16d4133560c6376
data/CHANGELOG.md CHANGED
@@ -2,4 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.1 (2023-03-20)
6
+
7
+ - Add Ruby Bytes project generator (see `templates/generator`).
8
+
9
+ ## 0.1.0 (2023-03-17) 🍀
10
+
11
+ - Initial release.
12
+
5
13
  [@palkan]: https://github.com/palkan
data/README.md CHANGED
@@ -6,24 +6,186 @@ Ruby Bytes is a tool to build application templates for Ruby and Rails applicati
6
6
 
7
7
  - Build complex templates consisting of multiple independent components.
8
8
  - Test templates with ease.
9
+ - Install application templates without Rails.
10
+ - Publish templates to [RailsBytes][].
9
11
 
10
- We also provide a GitHub action to deploy _compiled_ templates to [RailsBytes][].
12
+ We also provide a [GitHub action](#github-action) to compile and deploy templates continuously.
11
13
 
12
- Templates built with Ruby Bytes can be used with the `rails app:template` command or with a custom [Thor command](#thor-integration) (if you want to use a template in a Rails-less environment)
14
+ ## Examples
15
+
16
+ - [ruby-on-whales][]
17
+ - [view_component-contrib][]
18
+
19
+ See also examples in the [templates](https://github.com/palkan/rbytes/tree/master/templates) folder.
13
20
 
14
21
  ## Installation
15
22
 
16
- In your Gemfile:
23
+ To install templates, install the `rbytes` executable via the gem:
24
+
25
+ ```sh
26
+ gem install rbytes
27
+ ```
28
+
29
+ For templates development, add `rbytes` to your Gemfile or gemspec:
17
30
 
18
31
  ```ruby
19
32
  # Gemfile
20
33
  gem "rbytes"
21
34
  ```
22
35
 
23
- ## Building templates
36
+ ## Installing templates
37
+
38
+ You can use `rbytes install <url>` similarly to `rails app:template` but without needing to install Rails. It's useful if you want to use a template in a Rails-less environment.
39
+
40
+ Usage example:
41
+
42
+ ```sh
43
+ $ rbytes install https://railsbytes.com/script/x7msKX
44
+
45
+ Run template from: https://railsbytes.com/script/x7msKX
46
+ apply https://railsbytes.com/script/x7msKX
47
+ hello world from https://railsbytes.com 👋
48
+ ```
49
+
50
+ **IMPORTANT**: Not all templates from RailsBytes may be supported as of yet. Please, let us know if you find incompatibilities with `rails app:template`, so we can fix them.
51
+
52
+ You can also install Ruby Bytes as a plugin for Thor (see [Thor integration](#thor-integration)).
53
+
54
+ ## Writing templates
55
+
56
+ > The quickes way to get started with using Ruby Bytes to build templates is to use [our generator](templates/generator/).
57
+
58
+ Ruby Bytes adds partial support to Thor/Rails templates. For that, you can use `#include` and `#render` methods:
59
+
60
+ ```erb
61
+ say "Welcome to a custom Rails template!"
62
+
63
+ <%= include "setup_gems" %>
64
+
65
+ file "config/initializers/my-gem.rb", <%= code("initializer.rb") %>
66
+ ```
67
+
68
+ The `#include` helper simply injects the contents of the partial into the resulting file.
69
+
70
+ The `#code` method allows you to inject dynamic contents depending on the local variables defined. For example, given the following template and a partial:
71
+
72
+ ```erb
73
+ # _anycable.yml.tt
74
+ development:
75
+ broadcast_adapter: <%= cable_adapter %>
76
+
77
+ # template.rb
78
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
79
+
80
+ file "config/anycable.yml", <%= code("anycable.yml") %>
81
+ ```
82
+
83
+ The compiled template will like like this:
84
+
85
+ ```erb
86
+ cable_adapter = ask? "Which AnyCable pub/sub adapter do you want to use?"
87
+
88
+ file "config/anycable.yml", ERB.new(
89
+ *[
90
+ <<~'CODE'
91
+ development:
92
+ broadcast_adapter: <%= cable_adapter %>
93
+ CODE
94
+ ], trim_mode: "<>").result(binding)
95
+ ```
96
+
97
+ **NOTE:** By default, we assume that partials are stored next to the template's entrypoint. Partials may have "_" prefix and ".rb"/".tt" suffixes.
98
+
99
+ ### Compiling templates
100
+
101
+ You can compile a template by using the `rbytes` executable:
102
+
103
+ ```sh
104
+ $ rbytes compile path/to/template
105
+
106
+ <compiled template>
107
+ ```
108
+
109
+ You can also specify a custom partials directory:
110
+
111
+ ```sh
112
+ rbytes compile path/to/template --root=path/to/partials
113
+ ```
24
114
 
25
115
  ### Testing
26
116
 
117
+ We provide a Minitest integration to test your templates.
118
+
119
+ Here is an example usage:
120
+
121
+ ```ruby
122
+ require "ruby_bytes/test_case"
123
+
124
+ class TemplateTest < RubyBytes::TestCasee
125
+ # Specify root path for your template (for partials lookup)
126
+ root File.join(__dir__, "../template")
127
+
128
+ # You can test partials in isolation by declaring a custom template
129
+ template <<~RUBY
130
+ say "Hello from some partial"
131
+ <%= include "some_partial" %>
132
+ RUBY
133
+
134
+ def test_some_partial
135
+ run_generator do |output|
136
+ assert_file "application.rb"
137
+
138
+ assert_file_contains(
139
+ "application.rb",
140
+ <<~CODE
141
+ module Rails
142
+ class << self
143
+ def application
144
+ CODE
145
+ )
146
+
147
+ refute_file_contains(
148
+ "application.rb",
149
+ "Nothing"
150
+ )
151
+
152
+ assert_line_printed output, "Hello from some partial"
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ If you use prompt in your templates, you can prepopulate standard input:
159
+
160
+ ```ruby
161
+ class TemplateTest < RubyBytes::TestCasee
162
+ # Specify root path for your template (for partials lookup)
163
+ root File.join(__dir__, "../template")
164
+
165
+ # You can test partials in isolation by declaring a custom template
166
+ template <<~RUBY
167
+ say "Hello from some partial"
168
+ if yes?("Do you write tests?")
169
+ say "Gut"
170
+ else
171
+ say "Why not?"
172
+ end
173
+ RUBY
174
+
175
+ def test_prompt_yes
176
+ run_generator(input: ["y"]) do |output|
177
+ assert_line_printed output, "Gut"
178
+ end
179
+ end
180
+
181
+ def test_prompt_no
182
+ run_generator(input: ["n"]) do |output|
183
+ assert_line_printed output, "Why not?"
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
27
189
  ## Thor integration
28
190
 
29
191
  We provide a custom Thor command, which can be used to apply templates (similar to `rails app:template`).
@@ -46,6 +208,43 @@ Run template from: https://railsbytes.com/script/x7msKX
46
208
  hello world from https://railsbytes.com 👋
47
209
  ```
48
210
 
211
+ ## GitHub action
212
+
213
+ You can use our GitHub action to deploy your templates to RailsBytes.
214
+
215
+ Here is an example:
216
+
217
+ ```yml
218
+ name: Publish
219
+
220
+ on:
221
+ push:
222
+ tags:
223
+ - v*
224
+ workflow_dispatch:
225
+
226
+ jobs:
227
+ publish:
228
+ uses: palkan/rbytes/.github/workflows/railsbytes.yml@master
229
+ with:
230
+ template: templates/my-template.rb
231
+ secrets:
232
+ RAILS_BYTES_ACCOUNT_ID: "${{ secrets.RAILS_BYTES_ACCOUNT_ID }}"
233
+ RAILS_BYTES_TOKEN: "${{ secrets.RAILS_BYTES_TOKEN }}"
234
+ RAILS_BYTES_TEMPLATE_ID: "${{ secrets.RAILS_TEMPLATE_ID }}"
235
+ ```
236
+
237
+ ## Publishing manually
238
+
239
+ You can use the `rbytes publish` command to compile and publish a template to RailsBytes:
240
+
241
+ ```sh
242
+ RAILS_BYTES_ACCOUNT_ID=aaa \
243
+ RAILS_BYTES_TOKEN=bbb \
244
+ RAILS_BYTES_TEMPLATE_ID=ccc \
245
+ rbytes publish path/to/template
246
+ ```
247
+
49
248
  ## Contributing
50
249
 
51
250
  Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/rbytes](https://github.com/palkan/rbytes).
@@ -59,3 +258,5 @@ This gem is generated via [new-gem-generator](https://github.com/palkan/new-gem-
59
258
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
60
259
 
61
260
  [RailsBytes]: https://railsbytes.com
261
+ [ruby-on-whales]: https://github.com/evilmartians/ruby-on-whales
262
+ [view_component-contrib]: https://github.com/palkan/view_component-contrib
data/bin/rbytes ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ruby_bytes/cli"
4
+
5
+ begin
6
+ cli = RubyBytes::CLI.new
7
+ cli.run(*ARGV)
8
+ rescue => e
9
+ raise e if $DEBUG
10
+ STDERR.puts e.message
11
+ STDERR.puts e.backtrace.join("\n") if $DEBUG
12
+ exit 1
13
+ end
data/lib/rbytes.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rbytes/version"
3
+ require "ruby_bytes/version"
4
+ require "ruby_bytes/compiler"
5
+ require "ruby_bytes/publisher"
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbytes"
4
+ require "optparse"
5
+
6
+ module RubyBytes
7
+ class CLI
8
+ COMMANDS = %w[
9
+ compile
10
+ publish
11
+ install
12
+ ].freeze
13
+
14
+ def run(command, *args)
15
+ raise ArgumentError, "Unknown command: #{command}\nAvailable commands are: #{COMMANDS.join(", ")}\nRuby Bytes: v#{RubyBytes::VERSION}" unless COMMANDS.include?(command)
16
+
17
+ public_send(command, *args)
18
+ end
19
+
20
+ def compile(*args)
21
+ root = nil # rubocop:disable Lint/UselessAssignment
22
+
23
+ path, args = *args
24
+
25
+ OptionParser.new do |o|
26
+ o.on "-v", "--version", "Print version and exit" do |_arg|
27
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
28
+ exit(0)
29
+ end
30
+
31
+ o.on "--root [DIR]", "Location of partial template files" do
32
+ raise ArgumentError, "Directory not found: #{_1}" unless File.directory?(_1)
33
+ root = _1
34
+ end
35
+
36
+ o.on_tail "-h", "--help", "Show help" do
37
+ $stdout.puts <<~USAGE
38
+ rbytes compile PATH [options]
39
+
40
+ Options:
41
+ --root DIR Location of partial template files
42
+ USAGE
43
+
44
+ exit(0)
45
+ end
46
+ end.parse!(args || [])
47
+
48
+ raise ArgumentError, "File not found: #{path}" unless File.file?(path)
49
+
50
+ $stdout.puts Compiler.new(path, root: root).render
51
+ end
52
+
53
+ def publish(*args)
54
+ root = nil # rubocop:disable Lint/UselessAssignment
55
+
56
+ path, args = *args
57
+
58
+ OptionParser.new do |o|
59
+ o.on "-v", "--version", "Print version and exit" do |_arg|
60
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
61
+ exit(0)
62
+ end
63
+
64
+ o.on "--root [DIR]", "Location of partial template files" do
65
+ raise ArgumentError, "Directory not found: #{_1}" unless File.directory?(_1)
66
+ root = _1
67
+ end
68
+
69
+ o.on_tail "-h", "--help", "Show help" do
70
+ $stdout.puts <<~USAGE
71
+ rbytes publish PATH [options]
72
+
73
+ Options:
74
+ --root DIR Location of partial template files
75
+ USAGE
76
+
77
+ exit(0)
78
+ end
79
+ end.parse!(args || [])
80
+
81
+ raise ArgumentError, "File not found: #{path}" unless File.file?(path)
82
+
83
+ contents = Compiler.new(path, root: root).render
84
+
85
+ Publisher.new.call(contents)
86
+
87
+ $stdout.puts "Published successfully ✅"
88
+ end
89
+
90
+ def install(*args)
91
+ url, args = *args
92
+
93
+ OptionParser.new do |o|
94
+ o.on "-v", "--version", "Print version and exit" do |_arg|
95
+ $stdout.puts "Ruby Bytes: v#{RubyBytes::VERSION}"
96
+ exit(0)
97
+ end
98
+
99
+ o.on_tail "-h", "--help", "Show help" do
100
+ $stdout.puts <<~USAGE
101
+ rbytes install URL
102
+ USAGE
103
+
104
+ exit(0)
105
+ end
106
+ end.parse!(args || [])
107
+
108
+ raise ArgumentError, "Template URL or location must be provided" unless url
109
+
110
+ require "thor"
111
+ require "ruby_bytes/thor"
112
+
113
+ Rbytes::Base.source_paths << Dir.pwd
114
+ Rbytes.new.template(url)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module RubyBytes
6
+ class Compiler
7
+ attr_reader :path, :template, :root
8
+
9
+ def initialize(path, root: nil)
10
+ @path = path
11
+ raise ArgumentError, "There is no file at the path: #{path}" unless File.file?(path)
12
+
13
+ @template = File.read(path)
14
+ @root = root || File.dirname(File.expand_path(path))
15
+ end
16
+
17
+ def render(contents = template)
18
+ ERB.new(contents, trim_mode: "<>").result(binding)
19
+ end
20
+
21
+ def code(path)
22
+ contents = File.read(resolve_path(path))
23
+ %(ERB.new(
24
+ *[
25
+ <<~'TCODE'
26
+ #{contents}
27
+ TCODE
28
+ ], trim_mode: "<>").result(binding))
29
+ end
30
+
31
+ def include(path, indent: 0)
32
+ indented(render(File.read(resolve_path(path))), indent)
33
+ end
34
+
35
+ private
36
+
37
+ PATH_CANDIDATES = [
38
+ "%{path}",
39
+ "_%{path}",
40
+ "%{path}.rb",
41
+ "_%{path}.rb",
42
+ "%{path}.tt",
43
+ "_%{path}.tt"
44
+ ].freeze
45
+
46
+ def resolve_path(path)
47
+ PATH_CANDIDATES.each do |pattern|
48
+ resolved = File.join(root, pattern % {path: path})
49
+ return resolved if File.file?(resolved)
50
+ end
51
+
52
+ raise "File not found: #{path}"
53
+ end
54
+
55
+ def indented(content, multiplier = 2) # :doc:
56
+ spaces = " " * multiplier
57
+ content.each_line.map { |line| (!line.match?(/\S/)) ? line : "#{spaces}#{line}" }.join
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyBytes
4
+ class Publisher
5
+ attr_reader :account_id, :token, :template_id
6
+
7
+ def initialize(
8
+ account_id: ENV.fetch("RAILS_BYTES_ACCOUNT_ID"),
9
+ token: ENV.fetch("RAILS_BYTES_TOKEN"),
10
+ template_id: ENV.fetch("RAILS_BYTES_TEMPLATE_ID")
11
+ )
12
+ @account_id = account_id
13
+ @token = token
14
+ @template_id = template_id
15
+ end
16
+
17
+ def call(template)
18
+ require "net/http"
19
+ require "json"
20
+
21
+ path = "/api/v1/accounts/#{account_id}/templates/#{template_id}.json"
22
+ data = JSON.dump(script: template)
23
+
24
+ Net::HTTP.start("railsbytes.com", 443, use_ssl: true) do |http|
25
+ http.patch(
26
+ path,
27
+ data,
28
+ {
29
+ "Content-Type" => "application/json",
30
+ "Authorization" => "Bearer GGpxArFDSa2x3MT4BQNcyxG6"
31
+ }
32
+ )
33
+ end.then do |response|
34
+ raise "Failed to publish template: #{response.code} — #{response.message}" unless response.code == "200"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "fileutils"
5
+ require "stringio"
6
+ require "thor"
7
+
8
+ require "ruby_bytes/thor"
9
+
10
+ module RubyBytes
11
+ class TestCase < Minitest::Test
12
+ TMP_DIR = File.join(Dir.pwd, "tmp", "rbytes_test")
13
+
14
+ Rbytes::Base.source_paths << TMP_DIR
15
+
16
+ # Patch Thor::LineEditor to use Basic in tests
17
+ $rbytes_testing = false
18
+
19
+ Thor::LineEditor.singleton_class.prepend(Module.new do
20
+ def best_available
21
+ return super unless $rbytes_testing
22
+
23
+ Thor::LineEditor::Basic
24
+ end
25
+ end)
26
+
27
+ class << self
28
+ attr_reader :template_contents
29
+
30
+ def destination_root(val = nil)
31
+ if val
32
+ @destination_root = val
33
+ end
34
+
35
+ return @destination_root if instance_variable_defined?(:@destination_root)
36
+
37
+ @destination_root =
38
+ if superclass.respond_to?(:destination_root)
39
+ superclass.destination_root
40
+ else
41
+ TMP_DIR
42
+ end
43
+ end
44
+
45
+ def root(val = nil)
46
+ if val
47
+ @root = val
48
+ end
49
+
50
+ return @root if instance_variable_defined?(:@root)
51
+
52
+ @root =
53
+ if superclass.respond_to?(:root)
54
+ superclass.root
55
+ end
56
+ end
57
+
58
+ # Set the path to dummy app.
59
+ # Dummy app is copied to the temporary directory for every run
60
+ # and set as a destination root.
61
+ def dummy_app(val = nil)
62
+ if val
63
+ @dummy_app = val
64
+ end
65
+
66
+ return @dummy_app if instance_variable_defined?(:@dummy_app)
67
+
68
+ @dummy_app =
69
+ if superclass.respond_to?(:dummy_app)
70
+ superclass.dummy_app
71
+ end
72
+ end
73
+
74
+ def template(contents)
75
+ @template_contents = contents
76
+ end
77
+ end
78
+
79
+ attr_accessor :destination
80
+
81
+ def setup
82
+ FileUtils.rm_rf(TMP_DIR) if File.directory?(TMP_DIR)
83
+ FileUtils.mkdir_p(TMP_DIR)
84
+ end
85
+
86
+ def prepare_dummy
87
+ # Then, copy the dummy app if any
88
+ dummy = self.class.dummy_app
89
+ return unless dummy
90
+
91
+ return if @dummy_prepared
92
+
93
+ raise ArgumentError, "Dummy app must be a directory" unless File.directory?(dummy)
94
+
95
+ tmp_dummy_path = File.join(TMP_DIR, "dummy")
96
+ FileUtils.rm_rf(tmp_dummy_path) if File.directory?(tmp_dummy_path)
97
+ FileUtils.cp_r(dummy, tmp_dummy_path)
98
+ self.destination = tmp_dummy_path
99
+
100
+ if block_given?
101
+ Dir.chdir(tmp_dummy_path) { yield }
102
+ end
103
+
104
+ @dummy_prepared = true
105
+ end
106
+
107
+ def run_generator(input: [])
108
+ # First, compile the template (if not yet)
109
+ path = File.join(TMP_DIR, "current_template.rb")
110
+
111
+ if File.file?(path)
112
+ File.delete(path)
113
+ end
114
+
115
+ File.write(path, "")
116
+
117
+ rendered = Compiler.new(path, root: self.class.root).render(self.class.template_contents)
118
+ File.write(path, rendered)
119
+
120
+ self.destination = self.class.destination_root
121
+
122
+ prepare_dummy
123
+
124
+ original_stdout = $stdout
125
+ original_stdin = $stdin
126
+
127
+ if input.size > 0
128
+ $rbytes_testing = true
129
+
130
+ io = StringIO.new
131
+ input.each { io.puts(_1) }
132
+ io.rewind
133
+ $stdin = io
134
+ $stdin.sync = true
135
+ end
136
+
137
+ $stdout = StringIO.new
138
+ $stdout.sync = true
139
+
140
+ begin
141
+ Dir.chdir(destination) do
142
+ Rbytes::Base.new(
143
+ [destination], {}, {destination_root: destination}
144
+ ).apply("current_template.rb")
145
+ end
146
+ yield $stdout.string if block_given?
147
+ ensure
148
+ $stdout = original_stdout
149
+ $stdin = original_stdin
150
+ $rbytes_testing = false
151
+ @dummy_prepared = false
152
+ end
153
+ end
154
+
155
+ def assert_line_printed(io, line)
156
+ lines = io.lines
157
+
158
+ assert lines.any? { _1.include?(line) }, "Expected to print line: #{line}. Got: #{io}"
159
+ end
160
+
161
+ def assert_file_contains(path, body)
162
+ fullpath = File.join(destination, path)
163
+ assert File.file?(fullpath), "File not found: #{path}"
164
+
165
+ actual = File.read(fullpath)
166
+ assert_includes actual, body
167
+ end
168
+
169
+ def assert_file(path)
170
+ fullpath = File.join(destination, path)
171
+ assert File.file?(fullpath), "File not found: #{path}"
172
+ end
173
+
174
+ def refute_file(path)
175
+ fullpath = File.join(destination, path)
176
+ refute File.file?(fullpath), "File must not exist: #{path}"
177
+ end
178
+
179
+ def refute_file_contains(path, body)
180
+ fullpath = File.join(destination, path)
181
+ assert File.file?(fullpath), "File not found: #{path}"
182
+
183
+ actual = File.read(fullpath)
184
+ refute_includes actual, body
185
+ end
186
+ end
187
+ end