tina4ruby 3.13.89 → 3.13.91

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: c9a4969bc3af1b2a6f203d215043c4fb9497aa40e71a0f7d1c17d92eddeecea3
4
- data.tar.gz: 67a4a34e48ca3e167201ab06ac5cab96be36b4f4224a97f04cae0c58f700184c
3
+ metadata.gz: 3df8f30ccbf4fc6d984ec4af8fa3262045b16ac1f05c7516f6a6818691fad547
4
+ data.tar.gz: 1236689bdee9821f1994bc27d1a69684fd312cc774f33a2bba6b10fa57bd86d8
5
5
  SHA512:
6
- metadata.gz: a89938265a10f5b8f0a48966f370c55006937a236285fe6b8f2fd9c5f75464d5c4fc0fc65cf1583f76539e24cd095701e3c092d87fb9d1197978d1d9774f552c
7
- data.tar.gz: af228d8eebfab34598468e44f6962ac701280d4ad1207e4184f2eee6aabc631c5dda36753de7c99b1e8aea4dde7cff635fa2c44c30fff469532124cab0d9f11a
6
+ metadata.gz: c51b11ed1b2c9361c162ab6d54f999381d13b8b6be74f4cd94ebfa0d72492f0805f9428e9080ee06b2eb6efd52dc5fd72e1a9d073743d5ad03ec9582c3894b8b
7
+ data.tar.gz: 4f8265ec9da1f6bd427ed844d808e4e2c67ee55ec934a0d24ea37f40b1a88861ce86bc9c09b162cd534f6d879825ed31a05e0baf32e73322ea8584097d9b8346
data/lib/tina4/cli.rb CHANGED
@@ -233,6 +233,21 @@ module Tina4
233
233
  to_snake_case(name)
234
234
  end
235
235
 
236
+ # Called without --fields, the generators fall back to a single `name`
237
+ # string column. That default MUST be materialised here, in one place, and
238
+ # then flow into the model, the migration, the form, the view and the spec
239
+ # alike. It used to live only inside the model template, so `generate model
240
+ # X` / `generate crud X` wrote a model declaring `name` while the migration
241
+ # - built from the parsed field list, which was empty - created only id +
242
+ # created_at. The first write then failed with "no such column: name".
243
+ DEFAULT_FIELDS = [["name", "string"]].freeze
244
+
245
+ # Parsed --fields, or the default single `name` column when none given.
246
+ def fields_or_default(fields_str)
247
+ parsed = parse_fields(fields_str)
248
+ parsed.any? ? parsed : DEFAULT_FIELDS.map(&:dup)
249
+ end
250
+
236
251
  # Parse "name:string,price:float" -> [["name","string"], ["price","float"]]
237
252
  def parse_fields(fields_str)
238
253
  return [] if fields_str.nil? || fields_str.strip.empty?
@@ -1231,19 +1246,15 @@ module Tina4
1231
1246
  # ── Generator: model ─────────────────────────────────────────────────
1232
1247
 
1233
1248
  def generate_model(name, flags, emit_test: true)
1234
- fields = parse_fields(flags["fields"])
1249
+ fields = fields_or_default(flags["fields"])
1235
1250
  table = to_table_name(name)
1236
1251
  snake = to_snake_case(name)
1237
1252
 
1238
1253
  # Build field lines
1239
1254
  field_lines = [" integer_field :id, primary_key: true, auto_increment: true"]
1240
- if fields.any?
1241
- fields.each do |fname, ftype|
1242
- info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP["string"]
1243
- field_lines << " #{info[:orm]} :#{fname}"
1244
- end
1245
- else
1246
- field_lines << " string_field :name"
1255
+ fields.each do |fname, ftype|
1256
+ info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP["string"]
1257
+ field_lines << " #{info[:orm]} :#{fname}"
1247
1258
  end
1248
1259
  field_lines << " string_field :created_at"
1249
1260
 
@@ -1355,6 +1366,11 @@ module Tina4
1355
1366
  Tina4::Router.post "/api/#{route_path}" do |request, response|
1356
1367
  #{ext_create.chomp}
1357
1368
  item = #{model}.create(request.body)
1369
+ # create/save signal failure by RETURN VALUE, they do not raise -
1370
+ # unchecked, a failed write surfaces as an unrelated NoMethodError
1371
+ # on false and hides the real cause.
1372
+ next response.json({ error: "Could not create #{singular}" }, 400) if item == false
1373
+
1358
1374
  response.json(item.to_h, 201)
1359
1375
  end#{no_auth}
1360
1376
 
@@ -1368,7 +1384,8 @@ module Tina4
1368
1384
  setter = "#{'#'}{key}="
1369
1385
  item.send(setter, value) if item.respond_to?(setter)
1370
1386
  end
1371
- item.save
1387
+ next response.json({ error: "Could not update #{singular}" }, 400) if item.save == false
1388
+
1372
1389
  response.json(item.to_h)
1373
1390
  end#{no_auth}
1374
1391
 
@@ -1521,7 +1538,11 @@ module Tina4
1521
1538
  end
1522
1539
 
1523
1540
  # Build SQL columns from fields
1524
- fields = fields_override || parse_fields(flags["fields"])
1541
+ # An EMPTY array is truthy in Ruby, so a plain `fields_override || parse`
1542
+ # short-circuits to [] and the fallback never fires - that is exactly how
1543
+ # a model declaring `name` ended up with a column-less migration. Test for
1544
+ # content, not truthiness, so the semantics match Python/Node.
1545
+ fields = fields_override&.any? ? fields_override : parse_fields(flags["fields"])
1525
1546
  is_create = name.start_with?("create_") || !fields_override.nil?
1526
1547
 
1527
1548
  filename = "#{timestamp}_#{name}.sql"
@@ -1771,7 +1792,7 @@ module Tina4
1771
1792
  # ── Generator: form ──────────────────────────────────────────────────
1772
1793
 
1773
1794
  def generate_form(name, flags = {})
1774
- fields = parse_fields(flags["fields"])
1795
+ fields = fields_or_default(flags["fields"])
1775
1796
  table = to_table_name(name)
1776
1797
  route_name = "#{table}s"
1777
1798
 
@@ -1794,8 +1815,7 @@ module Tina4
1794
1815
 
1795
1816
  # Build form fields
1796
1817
  field_html = ""
1797
- form_fields = fields.any? ? fields : [["name", "string"]]
1798
- form_fields.each do |fname, ftype|
1818
+ fields.each do |fname, ftype|
1799
1819
  itype = input_types[ftype] || "text"
1800
1820
  label = fname.tr("_", " ").split.map(&:capitalize).join(" ")
1801
1821
  step = %w[float numeric decimal].include?(ftype) ? ' step="0.01"' : ""
@@ -1850,11 +1870,11 @@ module Tina4
1850
1870
  # ── Generator: view ──────────────────────────────────────────────────
1851
1871
 
1852
1872
  def generate_view(name, flags = {})
1853
- fields = parse_fields(flags["fields"])
1873
+ fields = fields_or_default(flags["fields"])
1854
1874
  table = to_table_name(name)
1855
1875
  route_name = "#{table}s"
1856
1876
 
1857
- cols = fields.any? ? fields.map { |f, _| f } : ["name"]
1877
+ cols = fields.map { |f, _| f }
1858
1878
 
1859
1879
  dir = "src/templates/pages"
1860
1880
  FileUtils.mkdir_p(dir)
@@ -2480,7 +2500,10 @@ module Tina4
2480
2500
 
2481
2501
  # model -> real SQLite roundtrip (create / read back / missing -> nil).
2482
2502
  def emit_model_test(model, table, fields)
2483
- fields = fields.empty? ? [["name", "string"]] : fields
2503
+ # Reuse the single DEFAULT_FIELDS constant rather than re-stating the
2504
+ # literal, so the co-emitted spec can never describe a shape the model
2505
+ # does not actually have.
2506
+ fields = fields.empty? ? DEFAULT_FIELDS.map(&:dup) : fields
2484
2507
  payload = fields.map { |fname, ftype| %("#{fname}" => #{sample_literal(ftype)}) }.join(", ")
2485
2508
  # Assert a STRING field round-trips (type-safe); else just the id round-trips
2486
2509
  # (avoids datetime/bool/float equality pitfalls on the read-back).
data/lib/tina4/metrics.rb CHANGED
@@ -254,7 +254,7 @@ module Tina4
254
254
  end
255
255
 
256
256
  lines = source.lines.map(&:chomp)
257
- loc = lines.count { |l| !l.strip.empty? && !l.strip.start_with?('#') }
257
+ loc = lines.count { |l| _code_line?(l) }
258
258
 
259
259
  # Extract imports (require/require_relative)
260
260
  imports = _extract_imports(lines)
@@ -509,7 +509,7 @@ module Tina4
509
509
  end
510
510
 
511
511
  lines = source.lines.map(&:chomp)
512
- loc = lines.count { |l| !l.strip.empty? && !l.strip.start_with?('#') }
512
+ loc = lines.count { |l| _code_line?(l) }
513
513
 
514
514
  functions = _extract_functions(source, tokens, lines)
515
515
  functions.sort_by! { |f| -f["complexity"] }
@@ -763,6 +763,17 @@ module Tina4
763
763
  buffers
764
764
  end
765
765
 
766
+ # True for a line that counts toward LOC: not blank, not a comment.
767
+ #
768
+ # The single definition of the rule. Method LOC used to ignore it and return a
769
+ # raw line span while file LOC excluded blanks and comments, so `loc` meant
770
+ # two different things in one payload - the dashboard sized bubbles in one
771
+ # unit and printed the method table in the other.
772
+ def self._code_line?(line)
773
+ stripped = line.strip
774
+ !stripped.empty? && !stripped.start_with?("#")
775
+ end
776
+
766
777
  def self._extract_functions(source, _tokens, _lines)
767
778
  functions = []
768
779
  # Operate on a neutralised copy: string/regex/comment CONTENT is blanked
@@ -811,7 +822,9 @@ module Tina4
811
822
  # Find method end and calculate LOC
812
823
  method_start = i
813
824
  method_end = _find_method_end(lines, i)
814
- method_loc = method_end - method_start + 1
825
+ # Code lines over the method's span, by the same rule as file LOC.
826
+ # Floor of 1: a one-line body must never report 0.
827
+ method_loc = [1, lines[method_start..method_end].count { |l| _code_line?(l) }].max
815
828
 
816
829
  # Calculate complexity for this method's body
817
830
  method_lines = lines[method_start..method_end]
@@ -842,6 +855,51 @@ module Tina4
842
855
  i += 1
843
856
  end
844
857
 
858
+ _charge_nested_complexity_to_the_nested_function(functions)
859
+ end
860
+
861
+ # Stop a function being charged for the complexity of the functions nested
862
+ # inside it.
863
+ #
864
+ # Each function's raw score is measured over its whole span, so a branch
865
+ # inside a nested function landed on BOTH that function and every function
866
+ # enclosing it. The over-count compounded with depth: a wrapper around twenty
867
+ # inner handlers absorbed the entire file's complexity and topped the
868
+ # offenders list, hiding the genuine hot spots.
869
+ #
870
+ # The correction is exact. A raw score is 1 + every decision in the span, so
871
+ # (raw - 1) is the total decision count of a function's whole subtree.
872
+ # Subtracting that for each DIRECT child leaves the function's own branches:
873
+ #
874
+ # own(F) = raw(F) - sum over direct children C of (raw(C) - 1)
875
+ #
876
+ # Blocks and lambdas are deliberately unaffected: they are not reported as
877
+ # functions of their own, so nothing subtracts them and their decisions stay
878
+ # with the method that contains them - moved, never lost.
879
+ def self._charge_nested_complexity_to_the_nested_function(functions)
880
+ return functions if functions.length < 2
881
+
882
+ last_line = ->(f) { f["line"] + [1, f["loc"]].max - 1 }
883
+ contains = ->(outer, inner) do
884
+ inner["line"] > outer["line"] && last_line.call(inner) <= last_line.call(outer)
885
+ end
886
+
887
+ raw = functions.map { |f| f["complexity"] }
888
+ functions.each_with_index do |outer, i|
889
+ subtract = 0
890
+ functions.each_with_index do |inner, j|
891
+ next if i == j || !contains.call(outer, inner)
892
+
893
+ # Direct child only: skip it if another function sits between the two,
894
+ # or its complexity would be subtracted twice.
895
+ nested_deeper = functions.each_with_index.any? do |mid, k|
896
+ k != i && k != j && contains.call(outer, mid) && contains.call(mid, inner)
897
+ end
898
+ subtract += raw[j] - 1 unless nested_deeper
899
+ end
900
+ outer["complexity"] = [1, raw[i] - subtract].max
901
+ end
902
+
845
903
  functions
846
904
  end
847
905