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 +4 -4
- data/lib/tina4/cli.rb +39 -16
- data/lib/tina4/metrics.rb +61 -3
- data/lib/tina4/public/js/tina4-dev-admin.min.js +179 -179
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3df8f30ccbf4fc6d984ec4af8fa3262045b16ac1f05c7516f6a6818691fad547
|
|
4
|
+
data.tar.gz: 1236689bdee9821f1994bc27d1a69684fd312cc774f33a2bba6b10fa57bd86d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
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
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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|
|
|
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|
|
|
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
|
-
|
|
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
|
|