kapusta 0.1.0 → 0.1.1

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.
@@ -5,13 +5,14 @@ module Kapusta
5
5
  WHITESPACE = [' ', "\t", "\n", "\r", "\f", "\v", ','].freeze
6
6
  DELIMS = ['(', ')', '[', ']', '{', '}', '"', ';'].freeze
7
7
 
8
- def self.read_all(source)
9
- new(source).read_all
8
+ def self.read_all(source, preserve_comments: false)
9
+ new(source, preserve_comments:).read_all
10
10
  end
11
11
 
12
- def initialize(source)
12
+ def initialize(source, preserve_comments: false)
13
13
  @src = source
14
14
  @pos = 0
15
+ @preserve_comments = preserve_comments
15
16
  end
16
17
 
17
18
  def read_all
@@ -20,7 +21,7 @@ module Kapusta
20
21
  skip_ws
21
22
  break if eof?
22
23
 
23
- forms << read_form
24
+ forms << read_next_item
24
25
  end
25
26
  forms
26
27
  end
@@ -46,7 +47,7 @@ module Kapusta
46
47
  char = peek
47
48
  if WHITESPACE.include?(char)
48
49
  advance
49
- elsif char == ';'
50
+ elsif !@preserve_comments && char == ';'
50
51
  advance until eof? || peek == "\n"
51
52
  else
52
53
  break
@@ -58,10 +59,21 @@ module Kapusta
58
59
  char.nil? || WHITESPACE.include?(char) || DELIMS.include?(char)
59
60
  end
60
61
 
62
+ def read_next_item
63
+ skip_ws
64
+ raise 'unexpected eof' if eof?
65
+
66
+ return read_comment if @preserve_comments && peek == ';'
67
+
68
+ read_form
69
+ end
70
+
61
71
  def read_form
62
72
  skip_ws
63
73
  raise 'unexpected eof' if eof?
64
74
 
75
+ return read_comment if @preserve_comments && peek == ';'
76
+
65
77
  form =
66
78
  case peek
67
79
  when '(' then read_list
@@ -84,7 +96,7 @@ module Kapusta
84
96
  raise 'unclosed (' if eof?
85
97
  break if peek == ')'
86
98
 
87
- items << read_form
99
+ items << read_next_item
88
100
  end
89
101
  advance
90
102
  List.new(items)
@@ -98,7 +110,7 @@ module Kapusta
98
110
  raise 'unclosed [' if eof?
99
111
  break if peek == ']'
100
112
 
101
- items << read_form
113
+ items << read_next_item
102
114
  end
103
115
  advance
104
116
  Vec.new(items)
@@ -106,32 +118,30 @@ module Kapusta
106
118
 
107
119
  def read_hash
108
120
  advance
109
- items = []
121
+ entries = []
122
+ pending = []
110
123
  loop do
111
124
  skip_ws
112
125
  raise 'unclosed {' if eof?
113
126
  break if peek == '}'
114
127
 
115
- items << read_form
128
+ item = read_next_item
129
+ if item.is_a?(Comment)
130
+ entries << item
131
+ next
132
+ end
133
+
134
+ pending << item
135
+ next unless pending.length == 2
136
+
137
+ entries << normalize_hash_pair(pending[0], pending[1])
138
+ pending.clear
116
139
  end
117
140
  advance
118
141
 
119
- pairs = []
120
- i = 0
121
- while i < items.length
122
- item = items[i]
123
- if item.is_a?(Sym) && item.name == ':'
124
- sym = items[i + 1]
125
- raise 'bad shorthand' unless sym.is_a?(Sym)
142
+ raise 'odd number of forms in hash' unless pending.empty?
126
143
 
127
- key = Kapusta.kebab_to_snake(sym.name).to_sym
128
- pairs << [key, sym]
129
- else
130
- pairs << [item, items[i + 1]]
131
- end
132
- i += 2
133
- end
134
- HashLit.new(pairs)
144
+ HashLit.new(entries)
135
145
  end
136
146
 
137
147
  def read_string
@@ -170,6 +180,12 @@ module Kapusta
170
180
  List.new([Sym.new('hashfn'), form])
171
181
  end
172
182
 
183
+ def read_comment
184
+ start = @pos
185
+ advance until eof? || peek == "\n"
186
+ Comment.new(@src[start...@pos].rstrip)
187
+ end
188
+
173
189
  def read_postfix(form)
174
190
  current = form
175
191
 
@@ -211,5 +227,16 @@ module Kapusta
211
227
  Sym.new(token)
212
228
  end
213
229
  end
230
+
231
+ def normalize_hash_pair(item, value)
232
+ if item.is_a?(Sym) && item.name == ':'
233
+ raise 'bad shorthand' unless value.is_a?(Sym)
234
+
235
+ key = Kapusta.kebab_to_snake(value.name).to_sym
236
+ [key, value]
237
+ else
238
+ [item, value]
239
+ end
240
+ end
214
241
  end
215
242
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
data/spec/cli_spec.rb CHANGED
@@ -74,4 +74,19 @@ RSpec.describe Kapusta::CLI do
74
74
 
75
75
  expect(output).to eq("Hello, Ada!\n")
76
76
  end
77
+
78
+ it 'prints the version with -v' do
79
+ output = capture_stdout do
80
+ described_class.start(['-v'])
81
+ end
82
+
83
+ expect(output).to eq("kapusta #{Kapusta::VERSION}\n")
84
+ end
85
+
86
+ it 'prints the version with --version from the executable' do
87
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, File.expand_path('../exe/kapusta', __dir__), '--version')
88
+
89
+ expect(status.success?).to eq(true), stderr
90
+ expect(stdout).to eq("kapusta #{Kapusta::VERSION}\n")
91
+ end
77
92
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'fileutils'
4
5
  require 'stringio'
5
6
 
6
7
  EXAMPLES_DIR = File.expand_path('../examples', __dir__)
@@ -38,6 +39,14 @@ RSpec.describe 'examples' do
38
39
  expect(run_example('binary-search.kap')).to eq("3\nnil\n")
39
40
  end
40
41
 
42
+ it 'blocks-and-kwargs.kap' do
43
+ path = File.expand_path('../tmp/blocks-and-kwargs.txt', EXAMPLES_DIR)
44
+ FileUtils.rm_f(path)
45
+
46
+ expect(run_example('blocks-and-kwargs.kap')).to eq("Ada\nLin\n2\n")
47
+ expect(File.exist?(path)).to eq(false)
48
+ end
49
+
41
50
  it 'block-sort.kap' do
42
51
  expect(run_example('block-sort.kap')).to eq("3, 2, 1\n")
43
52
  end
@@ -74,6 +83,14 @@ RSpec.describe 'examples' do
74
83
  expect(run_example('factorial.kap')).to eq("0\t1\n1\t1\n5\t120\n6\t720\n10\t3628800\n")
75
84
  end
76
85
 
86
+ it 'files.kap' do
87
+ path = File.expand_path('../tmp/file-io-example.txt', EXAMPLES_DIR)
88
+ FileUtils.rm_f(path)
89
+
90
+ expect(run_example('files.kap')).to eq("Ada\nLin\n2\n")
91
+ expect(File.exist?(path)).to eq(false)
92
+ end
93
+
77
94
  it 'fib.kap' do
78
95
  expect(run_example('fib.kap')).to eq("55\n")
79
96
  end
@@ -95,6 +112,10 @@ RSpec.describe 'examples' do
95
112
  expect(run_example('hashfn.kap')).to eq("5\n21\n")
96
113
  end
97
114
 
115
+ it 'inheritance.kap' do
116
+ expect(run_example('inheritance.kap')).to eq("true\tanimalia\tPoppy the dog\twoof\n")
117
+ end
118
+
98
119
  it 'leap-year.kap' do
99
120
  expect(run_example('leap-year.kap')).to eq("true\n")
100
121
  end
@@ -199,6 +220,10 @@ RSpec.describe 'examples' do
199
220
  it 'two-sum.kap' do
200
221
  expect(run_example('two-sum.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
201
222
  end
223
+
224
+ it 'threading.kap' do
225
+ expect(run_example('threading.kap')).to eq("[Ada Lovelace]!\t<Ada!>\tnil\tATSUPAK\tnil\n")
226
+ end
202
227
  end
203
228
 
204
229
  RSpec.describe Kapusta do
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'kapusta/formatter'
5
+ require 'open3'
6
+ require 'rbconfig'
5
7
  require 'stringio'
6
8
  require 'tmpdir'
7
9
 
@@ -128,16 +130,123 @@ RSpec.describe Kapusta::Formatter do
128
130
  expect(error_output).to include('Cannot use --fix with stdin (-).')
129
131
  end
130
132
 
131
- it 'rejects comments instead of dropping them' do
133
+ it 'prints the version with -v' do
134
+ output = capture_stdout do
135
+ expect(described_class.new(['-v']).run).to eq(0)
136
+ end
137
+
138
+ expect(output).to eq("kapfmt #{Kapusta::VERSION}\n")
139
+ end
140
+
141
+ it 'prints the version with --version from the executable' do
142
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, File.expand_path('../exe/kapfmt', __dir__), '--version')
143
+
144
+ expect(status.success?).to eq(true), stderr
145
+ expect(stdout).to eq("kapfmt #{Kapusta::VERSION}\n")
146
+ end
147
+
148
+ it 'preserves top-level comments and comments inside forms' do
132
149
  Dir.mktmpdir do |dir|
133
150
  path = File.join(dir, 'sample.kap')
134
- File.write(path, "(fn main [] ; comment\n (print 1))\n")
151
+ File.write(path, <<~KAP)
152
+ ; entry point
153
+ (fn main [] ; inline body comment
154
+ (print 1))
155
+ KAP
135
156
 
136
- error_output = capture_stderr do
137
- expect(described_class.new([path]).run).to eq(1)
157
+ output = capture_stdout do
158
+ expect(described_class.new([path]).run).to eq(0)
159
+ end
160
+
161
+ expect(output).to eq(<<~KAP)
162
+ ; entry point
163
+ (fn main []
164
+ ; inline body comment
165
+ (print 1))
166
+ KAP
167
+ end
168
+ end
169
+
170
+ it 'normalizes end-of-line comments into standalone indented comments' do
171
+ Dir.mktmpdir do |dir|
172
+ path = File.join(dir, 'sample.kap')
173
+ File.write(path, <<~KAP)
174
+ (let [words ["red" "green" "blue"]] ; source data
175
+ (-> words ; start pipeline
176
+ (: :select (fn [w] (< (length w) 5))) ; keep short words
177
+ (: :map (fn [w] (w.upcase)))))
178
+ KAP
179
+
180
+ output = capture_stdout do
181
+ expect(described_class.new([path]).run).to eq(0)
182
+ end
183
+
184
+ expect(output).to eq(<<~KAP)
185
+ (let [words ["red" "green" "blue"]]
186
+ ; source data
187
+ (-> words
188
+ ; start pipeline
189
+ (: :select (fn [w] (< (length w) 5)))
190
+ ; keep short words
191
+ (: :map (fn [w] (w.upcase)))))
192
+ KAP
193
+ end
194
+ end
195
+
196
+ it 'preserves comments in multiline collections' do
197
+ Dir.mktmpdir do |dir|
198
+ path = File.join(dir, 'sample.kap')
199
+ File.write(path, <<~KAP)
200
+ (let [profile {:name "Ada"
201
+ ; active user
202
+ :active true}
203
+ ; next binding
204
+ role "Engineer"]
205
+ (print profile role))
206
+ KAP
207
+
208
+ output = capture_stdout do
209
+ expect(described_class.new([path]).run).to eq(0)
138
210
  end
139
211
 
140
- expect(error_output).to include('kapfmt does not support comments yet.')
212
+ expect(output).to eq(<<~KAP)
213
+ (let
214
+ [
215
+ profile
216
+ {
217
+ :name "Ada"
218
+ ; active user
219
+ :active true}
220
+ ; next binding
221
+ role
222
+ "Engineer"]
223
+ (print profile role))
224
+ KAP
225
+ end
226
+ end
227
+
228
+ it 'preserves indented comments in nested bodies' do
229
+ Dir.mktmpdir do |dir|
230
+ path = File.join(dir, 'sample.kap')
231
+ File.write(path, <<~KAP)
232
+ (fn classify [score]
233
+ (if (> score 90)
234
+ ; fast path for top scores
235
+ :great
236
+ :ok))
237
+ KAP
238
+
239
+ output = capture_stdout do
240
+ expect(described_class.new([path]).run).to eq(0)
241
+ end
242
+
243
+ expect(output).to eq(<<~KAP)
244
+ (fn classify [score]
245
+ (if (> score 90)
246
+ ; fast path for top scores
247
+ :great
248
+ :ok))
249
+ KAP
141
250
  end
142
251
  end
143
252
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -29,6 +29,7 @@ files:
29
29
  - examples/anagram.kap
30
30
  - examples/binary-search.kap
31
31
  - examples/block-sort.kap
32
+ - examples/blocks-and-kwargs.kap
32
33
  - examples/calc.kap
33
34
  - examples/counter.kap
34
35
  - examples/describe.kap
@@ -39,10 +40,12 @@ files:
39
40
  - examples/exceptions.kap
40
41
  - examples/factorial.kap
41
42
  - examples/fib.kap
43
+ - examples/files.kap
42
44
  - examples/fizzbuzz.kap
43
45
  - examples/gcd.kap
44
46
  - examples/greet.kap
45
47
  - examples/hashfn.kap
48
+ - examples/inheritance.kap
46
49
  - examples/kwargs.kap
47
50
  - examples/leap-year.kap
48
51
  - examples/match.kap
@@ -64,6 +67,7 @@ files:
64
67
  - examples/squares.kap
65
68
  - examples/stack.kap
66
69
  - examples/sum.kap
70
+ - examples/threading.kap
67
71
  - examples/tset.kap
68
72
  - examples/two-sum.kap
69
73
  - exe/kapfmt
@@ -93,10 +97,13 @@ files:
93
97
  - spec/examples_spec.rb
94
98
  - spec/formatter_spec.rb
95
99
  - spec/spec_helper.rb
96
- homepage:
100
+ homepage: https://github.com/evmorov/kapusta
97
101
  licenses: []
98
102
  metadata:
99
103
  rubygems_mfa_required: 'true'
104
+ homepage_uri: https://github.com/evmorov/kapusta
105
+ source_code_uri: https://github.com/evmorov/kapusta
106
+ bug_tracker_uri: https://github.com/evmorov/kapusta/issues
100
107
  post_install_message:
101
108
  rdoc_options: []
102
109
  require_paths: