en14960 0.2.3 → 0.4.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: fedeb5f36f6f2e3f2ac74b39a18ad7ead640a520917d4681abe23403a477de7f
4
- data.tar.gz: 62b331bcad579a79b39039ba9a4fc8a3f8a15bef7bdf785e4e336434253e0112
3
+ metadata.gz: fda15828cb9885998e0aa06bb8cff2621e0dd3cbdd5ed5a717a102988f04ecb9
4
+ data.tar.gz: c9d0d2184b17fad322ca86a7ac19e978c7918ef1d2d412881072feb1ee995491
5
5
  SHA512:
6
- metadata.gz: c0195422eb49e92201c750a030003620844f798426143b4ed8b58d80fd1ff277ae8e8d735bb2edea76ebf146b24669adf67a0750ebc1f81f7683e5de33cd782a
7
- data.tar.gz: 8d0892f9d9c7522c43d4bfb8c7b16f80e947e8a36ea75b3126fcdf7e06470b3ecd6d6067a6515eec337fa739a2d98f9b218d3ad23b2ebae17a044494735ce223
6
+ metadata.gz: bf879fdb5e594c2078c14959a2d2c1040afcefe051edfa42156b623fc0d66583ee48c8baf2ca81f91894cacbfee840deb4c7f4863cceb9260c927305a9332ed8
7
+ data.tar.gz: 73699ce3ae6651564d4641b6746e8819b57c3a0c8938990bd11b4783156bfa5f0fd339281eb00518d3abe23f2284b56f1a61de11612b6692e0d2b32fea3ffa1c
File without changes
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Full Sorbet type signatures for all public APIs
12
+ - Runtime type checking with sorbet-runtime
13
+ - Strongly typed CalculatorResponse using T::Struct
14
+ - Type safety documentation in README
15
+ - Test file demonstrating type-safe usage
16
+
17
+ ### Changed
18
+ - CalculatorResponse now extends T::Struct instead of using Data.define
19
+ - All modules and classes now include Sorbet type annotations
20
+ - Improved type safety for method parameters and return values
21
+
8
22
  ## [0.1.0] - 2025-01-28
9
23
 
10
24
  ### Added
data/README.md CHANGED
@@ -10,6 +10,7 @@ A Ruby gem providing calculators and validators for BS EN 14960:2019 - the Briti
10
10
  - **Slide Safety**: Calculate runout distances and wall height requirements
11
11
  - **User Capacity**: Calculate safe user capacity based on play area and user heights
12
12
  - **Material Validation**: Validate material specifications against EN 14960 requirements
13
+ - **Type Safety**: Full Sorbet type signatures for all public APIs ensuring type safety and better IDE support
13
14
 
14
15
  ## Installation
15
16
 
@@ -170,6 +171,35 @@ EN14960::Constants::MATERIAL_STANDARDS
170
171
  EN14960::Constants::ANCHOR_CALCULATION_CONSTANTS
171
172
  ```
172
173
 
174
+ ## Type Safety with Sorbet
175
+
176
+ This gem includes full Sorbet type signatures for improved type safety and IDE support:
177
+
178
+ ```ruby
179
+ # All public methods have type signatures
180
+ sig { params(length: T.any(Float, Integer), width: T.any(Float, Integer), height: T.any(Float, Integer)).returns(CalculatorResponse) }
181
+ def calculate_anchors(length:, width:, height:)
182
+ # ...
183
+ end
184
+
185
+ # Return types are strongly typed
186
+ result = EN14960.calculate_anchors(length: 10, width: 8, height: 3)
187
+ result.value # Integer
188
+ result.value_suffix # String
189
+ result.breakdown # Array[Array[String]]
190
+ ```
191
+
192
+ The gem uses `T::Struct` for data classes providing runtime type checking:
193
+
194
+ ```ruby
195
+ # CalculatorResponse is a typed struct
196
+ class CalculatorResponse < T::Struct
197
+ const :value, T.any(Integer, Float, String, T::Array[T.untyped])
198
+ const :value_suffix, String, default: ""
199
+ const :breakdown, T::Array[T::Array[String]], default: []
200
+ end
201
+ ```
202
+
173
203
  ## EN 14960:2019 Compliance
174
204
 
175
205
  This gem implements calculations based on BS EN 14960:2019, including:
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module EN14960
7
+ module API
8
+ extend T::Sig
9
+
10
+ class << self
11
+ extend T::Sig
12
+
13
+ sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
14
+ def calculate_anchors(length:, width:, height:)
15
+ Calculators::AnchorCalculator.calculate(length: length, width: width, height: height)
16
+ end
17
+
18
+ sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
19
+ def calculate_slide_runout(platform_height, has_stop_wall: false)
20
+ Calculators::SlideCalculator.calculate_required_runout(platform_height, has_stop_wall: has_stop_wall)
21
+ end
22
+
23
+ sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
24
+ def calculate_wall_height(platform_height, user_height, has_permanent_roof = nil)
25
+ Calculators::SlideCalculator.calculate_wall_height_requirements(
26
+ platform_height,
27
+ user_height,
28
+ has_permanent_roof
29
+ )
30
+ end
31
+
32
+ sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
33
+ def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0.0)
34
+ Calculators::UserCapacityCalculator.calculate(
35
+ length,
36
+ width,
37
+ max_user_height,
38
+ negative_adjustment_area
39
+ )
40
+ end
41
+
42
+ sig { params(diameter_mm: Float).returns(T::Boolean) }
43
+ def valid_rope_diameter?(diameter_mm)
44
+ Validators::MaterialValidator.valid_rope_diameter?(diameter_mm)
45
+ end
46
+
47
+ sig { returns(T::Hash[Symbol, T.untyped]) }
48
+ def height_categories
49
+ Constants::HEIGHT_CATEGORIES
50
+ end
51
+
52
+ sig { returns(T::Hash[Symbol, T.untyped]) }
53
+ def material_standards
54
+ Constants::MATERIAL_STANDARDS
55
+ end
56
+
57
+ sig {
58
+ params(
59
+ unit_length: Float,
60
+ unit_width: Float,
61
+ play_area_length: Float,
62
+ play_area_width: Float,
63
+ negative_adjustment_area: Float
64
+ ).returns(T::Hash[Symbol, T.untyped])
65
+ }
66
+ def validate_play_area(
67
+ unit_length:,
68
+ unit_width:,
69
+ play_area_length:,
70
+ play_area_width:,
71
+ negative_adjustment_area:
72
+ )
73
+ Validators::PlayAreaValidator.validate(
74
+ unit_length: unit_length,
75
+ unit_width: unit_width,
76
+ play_area_length: play_area_length,
77
+ play_area_width: play_area_width,
78
+ negative_adjustment_area: negative_adjustment_area
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "../constants"
4
6
  require_relative "../models/calculator_response"
5
7
 
6
8
  module EN14960
7
9
  module Calculators
8
10
  module AnchorCalculator
11
+ extend T::Sig
9
12
  extend self
10
13
 
14
+ sig { params(area_m2: Float).returns(Integer) }
11
15
  def calculate_required_anchors(area_m2)
12
16
  # EN 14960-1:2019 Annex A (Lines 1175-1210) - Anchor calculation formula
13
17
  # Force = 0.5 × Cw × ρ × V² × A
14
18
  # Where: Cw = 1.5, ρ = 1.24 kg/m³, V = 11.1 m/s (Lines 1194-1199)
15
19
  # Number of anchors = Force / 1600N (Line 450 - each anchor withstands 1600N)
16
- return 0 if area_m2.nil? || area_m2 <= 0
20
+ return 0 if area_m2 <= 0
17
21
 
18
22
  # Pre-calculated: 0.5 × 1.5 × 1.24 × 11.1² ≈ 114
19
23
  area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
@@ -23,6 +27,7 @@ module EN14960
23
27
  ((area_m2.to_f * area_coeff * safety_mult) / base_div).ceil
24
28
  end
25
29
 
30
+ sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
26
31
  def calculate(length:, width:, height:)
27
32
  # EN 14960-1:2019 Lines 1175-1210 (Annex A) - Calculate exposed surface areas
28
33
  front_area = (width * height).round(1)
@@ -67,6 +72,7 @@ module EN14960
67
72
  )
68
73
  end
69
74
 
75
+ sig { returns(String) }
70
76
  def anchor_formula_text
71
77
  area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
72
78
  base_div = Constants::ANCHOR_CALCULATION_CONSTANTS[:base_divisor]
@@ -74,6 +80,7 @@ module EN14960
74
80
  "((Area × #{area_coeff} × #{safety_fact}) ÷ #{base_div})"
75
81
  end
76
82
 
83
+ sig { returns(String) }
77
84
  def anchor_calculation_description
78
85
  "Anchors must be calculated based on the play area to ensure adequate ground restraint for wind loads."
79
86
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "../constants"
4
6
  require_relative "../models/calculator_response"
5
7
 
6
8
  module EN14960
7
9
  module Calculators
8
10
  module SlideCalculator
11
+ extend T::Sig
9
12
  extend self
10
13
 
11
14
  # Simple calculation method that returns just the numeric value
15
+ sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(Float) }
12
16
  def calculate_runout_value(platform_height, has_stop_wall: false)
13
- return 0 if platform_height.nil? || platform_height <= 0
17
+ return 0.0 if platform_height <= 0
14
18
 
15
19
  height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
16
20
  minimum_runout = Constants::RUNOUT_CALCULATION_CONSTANTS[:minimum_runout_meters]
@@ -22,13 +26,14 @@ module EN14960
22
26
  has_stop_wall ? base_runout + stop_wall_add : base_runout
23
27
  end
24
28
 
29
+ sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
25
30
  def calculate_required_runout(platform_height, has_stop_wall: false)
26
31
  # EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
27
32
  # Line 934-935: The runout distance must be at least half the height of the slide's
28
33
  # highest platform (measured from ground level), with an absolute minimum of 300mm
29
34
  # Line 936: If a stop-wall is installed at the runout's end, an additional
30
35
  # 50cm must be added to the total runout length
31
- return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height.nil? || platform_height <= 0
36
+ return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height <= 0
32
37
 
33
38
  # Get constants
34
39
  height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
@@ -59,13 +64,13 @@ module EN14960
59
64
  )
60
65
  end
61
66
 
67
+ sig { params(platform_height: Float, user_height: Float, containing_wall_height: Float, has_permanent_roof: T::Boolean).returns(T::Boolean) }
62
68
  def meets_height_requirements?(platform_height, user_height, containing_wall_height, has_permanent_roof)
63
69
  # EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
64
70
  # Lines 859-860: Containing walls become mandatory for platforms exceeding 0.6m in height
65
71
  # Lines 861-862: Platforms between 0.6m and 3.0m need walls at least as tall as the maximum user height
66
72
  # Lines 863-864: Platforms between 3.0m and 6.0m require walls at least 1.25 times the maximum user height OR a permanent roof
67
73
  # Lines 865-866: Platforms over 6.0m must have both containing walls and a permanent roof structure
68
- return false if platform_height.nil? || user_height.nil? || containing_wall_height.nil? || has_permanent_roof.nil?
69
74
 
70
75
  enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
71
76
  thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
@@ -86,16 +91,17 @@ module EN14960
86
91
  end
87
92
  end
88
93
 
94
+ sig { params(runout_length: Float, platform_height: Float, has_stop_wall: T::Boolean).returns(T::Boolean) }
89
95
  def meets_runout_requirements?(runout_length, platform_height, has_stop_wall: false)
90
96
  # EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
91
97
  # Lines 934-935: The runout area must extend at least half the platform's height
92
98
  # or 300mm (whichever is greater) to allow users to decelerate safely
93
- return false if runout_length.nil? || platform_height.nil?
94
99
 
95
100
  required_runout = calculate_runout_value(platform_height, has_stop_wall: has_stop_wall)
96
101
  runout_length >= required_runout
97
102
  end
98
103
 
104
+ sig { returns(String) }
99
105
  def slide_runout_formula_text
100
106
  ratio_constant = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
101
107
  height_ratio = (ratio_constant * 100).to_i
@@ -104,9 +110,10 @@ module EN14960
104
110
  "#{height_ratio}% of platform height, minimum #{min_runout}mm"
105
111
  end
106
112
 
113
+ sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
107
114
  def calculate_wall_height_requirements(platform_height, user_height, has_permanent_roof = nil)
108
115
  # EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
109
- return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height.nil? || user_height.nil? || platform_height <= 0 || user_height <= 0
116
+ return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height <= 0 || user_height <= 0
110
117
 
111
118
  # Get requirement details and breakdown
112
119
  requirement_details = get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
@@ -121,6 +128,7 @@ module EN14960
121
128
  )
122
129
  end
123
130
 
131
+ sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(T::Hash[Symbol, T.untyped]) }
124
132
  def get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
125
133
  thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
126
134
  enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
@@ -204,19 +212,22 @@ module EN14960
204
212
  end
205
213
  end
206
214
 
215
+ sig { params(platform_height: Float).returns(T::Boolean) }
207
216
  def requires_permanent_roof?(platform_height)
208
217
  # EN 14960-1:2019 Section 4.2.9 (Lines 865-866)
209
218
  # Inflatable structures with platforms higher than 6.0m must be equipped
210
219
  # with both containing walls and a permanent roof
211
220
  threshold = Constants::SLIDE_HEIGHT_THRESHOLDS[:enhanced_walls]
212
- !platform_height.nil? && platform_height > threshold
221
+ platform_height > threshold
213
222
  end
214
223
 
224
+ sig { returns(String) }
215
225
  def wall_height_requirement
216
226
  multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
217
227
  "Containing walls required #{multiplier} times user height"
218
228
  end
219
229
 
230
+ sig { returns(T::Hash[Symbol, T::Hash[Symbol, String]]) }
220
231
  def slide_calculations
221
232
  # EN 14960:2019 - Comprehensive slide safety requirements
222
233
  {
@@ -243,6 +254,7 @@ module EN14960
243
254
 
244
255
  private
245
256
 
257
+ sig { params(platform_height: Float, user_height: Float).returns(Float) }
246
258
  def extract_required_wall_height(platform_height, user_height)
247
259
  thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
248
260
  enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "../constants"
4
6
  require_relative "../models/calculator_response"
5
7
 
6
8
  module EN14960
7
9
  module Calculators
8
10
  module UserCapacityCalculator
11
+ extend T::Sig
9
12
  extend self
10
13
 
14
+ sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
11
15
  def calculate(length, width, max_user_height = nil, negative_adjustment_area = 0)
12
- return default_result if length.nil? || width.nil?
16
+ return default_result if length <= 0 || width <= 0
13
17
 
14
18
  total_area = (length * width).round(2)
15
19
  negative_adjustment_area = negative_adjustment_area.to_f.abs
16
- usable_area = [total_area - negative_adjustment_area, 0].max.round(2)
20
+ usable_area = [total_area - negative_adjustment_area, 0.0].max.round(2)
17
21
 
18
22
  breakdown = build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
19
23
  capacities = calculate_capacities(usable_area, max_user_height, breakdown)
@@ -27,6 +31,7 @@ module EN14960
27
31
 
28
32
  private
29
33
 
34
+ sig { params(length: Float, width: Float, total_area: Float, negative_adjustment_area: Float, usable_area: Float).returns(T::Array[T::Array[String]]) }
30
35
  def build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
31
36
  breakdown = []
32
37
  formatted_length = format_number(length)
@@ -47,6 +52,7 @@ module EN14960
47
52
  breakdown
48
53
  end
49
54
 
55
+ sig { params(usable_area: Float, max_user_height: T.nilable(Float), breakdown: T::Array[T::Array[String]]).returns(T::Hash[Symbol, Integer]) }
50
56
  def calculate_capacities(usable_area, max_user_height, breakdown)
51
57
  capacities = {}
52
58
 
@@ -71,6 +77,7 @@ module EN14960
71
77
  capacities
72
78
  end
73
79
 
80
+ sig { returns(CalculatorResponse) }
74
81
  def default_result
75
82
  CalculatorResponse.new(
76
83
  value: default_capacity,
@@ -79,6 +86,7 @@ module EN14960
79
86
  )
80
87
  end
81
88
 
89
+ sig { returns(T::Hash[Symbol, Integer]) }
82
90
  def default_capacity
83
91
  {
84
92
  users_1000mm: 0,
@@ -88,6 +96,7 @@ module EN14960
88
96
  }
89
97
  end
90
98
 
99
+ sig { params(number: Float).returns(String) }
91
100
  def format_number(number)
92
101
  # Remove trailing zeros after decimal point
93
102
  formatted = sprintf("%.1f", number)
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
2
5
 
3
6
  module EN14960
4
7
  module Constants
8
+ extend T::Sig
5
9
  # Height category constants based on EN 14960:2019
6
10
  HEIGHT_CATEGORIES = {
7
11
  1000 => {label: "1.0m (Young children)", max_users: :calculate_by_area},
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
2
5
 
3
6
  module EN14960
4
7
  # Data class for calculator responses
5
8
  # Provides a consistent structure for all calculator results
6
- CalculatorResponse = Data.define(:value, :value_suffix, :breakdown) do
7
- def initialize(value:, value_suffix: "", breakdown: [])
8
- super
9
- end
9
+ class CalculatorResponse < T::Struct
10
+ extend T::Sig
10
11
 
12
+ const :value, T.any(Integer, Float, String, T::Hash[Symbol, Integer], T::Array[T.untyped])
13
+ const :value_suffix, String, default: ""
14
+ const :breakdown, T::Array[T::Array[String]], default: []
15
+
16
+ sig { returns(T::Hash[Symbol, T.untyped]) }
11
17
  def to_h
12
18
  {
13
19
  value: value,
@@ -16,6 +22,9 @@ module EN14960
16
22
  }
17
23
  end
18
24
 
19
- alias_method :as_json, :to_h
25
+ sig { returns(T::Hash[Symbol, T.untyped]) }
26
+ def as_json
27
+ to_h
28
+ end
20
29
  end
21
30
  end
@@ -1,17 +1,29 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
2
5
 
3
6
  module EN14960
4
7
  module SourceCode
8
+ extend T::Sig
9
+
10
+ sig { params(method_name: Symbol, module_name: Module, additional_methods: T::Array[Symbol]).returns(String) }
5
11
  def self.get_method_source(method_name, module_name, additional_methods = [])
6
- method_obj = module_name.method(method_name)
7
- source_location = method_obj.source_location
12
+ base_dir = File.expand_path("..", __FILE__)
13
+
14
+ ruby_files = Dir.glob(File.join(base_dir, "**", "*.rb"))
8
15
 
9
- return "Source code not available" unless source_location
16
+ file_path, line_number = find_method_in_files(ruby_files, method_name)
10
17
 
11
- file_path, line_number = source_location
12
- return "Source file not found" unless File.exist?(file_path)
18
+ unless file_path
19
+ raise StandardError, "Source code not available for method: #{method_name}"
20
+ end
21
+
22
+ unless File.exist?(file_path)
23
+ raise StandardError, "Source file not found: #{file_path}"
24
+ end
13
25
 
14
- lines = File.readlines(file_path)
26
+ lines = File.readlines(file_path, encoding: "UTF-8")
15
27
 
16
28
  constants_code = ""
17
29
  module_constants = get_module_constants(module_name, method_name)
@@ -27,10 +39,9 @@ module EN14960
27
39
 
28
40
  additional_methods.each do |additional_method|
29
41
  if module_name.respond_to?(additional_method)
30
- additional_obj = module_name.method(additional_method)
31
- additional_location = additional_obj.source_location
32
- if additional_location && additional_location[0] == file_path
33
- additional_lines = extract_method_lines(lines, additional_location[1] - 1, additional_method)
42
+ method_line_idx = lines.index { |line| line.strip =~ /^def\s+(self\.)?#{Regexp.escape(additional_method.to_s)}(\(|$|\s)/ }
43
+ if method_line_idx
44
+ additional_lines = extract_method_lines(lines, method_line_idx, additional_method)
34
45
  methods_code += strip_consistent_indentation(additional_lines.join("")) + "\n\n"
35
46
  end
36
47
  end
@@ -50,12 +61,31 @@ module EN14960
50
61
  output
51
62
  end
52
63
 
64
+ sig { params(files: T::Array[String], method_name: Symbol).returns(T.nilable([String, Integer])) }
65
+ private_class_method def self.find_method_in_files(files, method_name)
66
+ files.each do |path|
67
+ if File.exist?(path)
68
+ content = File.read(path, encoding: "UTF-8")
69
+ if content.match?(/def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|\s|$)/)
70
+ lines = File.readlines(path, encoding: "UTF-8")
71
+ line_idx = lines.index { |line| line.strip =~ /^def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|$|\s)/ }
72
+ if line_idx
73
+ return [path, line_idx + 1]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ nil
79
+ end
80
+
81
+ sig { params(module_name: Module, method_name: Symbol).returns(T::Array[Symbol]) }
53
82
  private_class_method def self.get_module_constants(module_name, method_name)
54
83
  module_name.constants.select do |const_name|
55
84
  module_name.const_get(const_name).is_a?(Hash)
56
85
  end
57
86
  end
58
87
 
88
+ sig { params(lines: T::Array[String], constant_name: Symbol).returns(String) }
59
89
  private_class_method def self.extract_constant_definition(lines, constant_name)
60
90
  constant_lines = []
61
91
  in_constant = false
@@ -82,6 +112,7 @@ module EN14960
82
112
  constant_lines.join("")
83
113
  end
84
114
 
115
+ sig { params(lines: T::Array[String], start_line: Integer, method_name: Symbol).returns(T::Array[String]) }
85
116
  private_class_method def self.extract_method_lines(lines, start_line, method_name)
86
117
  method_lines = []
87
118
  current_line = start_line
@@ -89,7 +120,7 @@ module EN14960
89
120
 
90
121
  while current_line < lines.length
91
122
  line = lines[current_line]
92
- if line.strip.start_with?("def #{method_name}")
123
+ if /^def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|$|\s)/.match?(line.strip)
93
124
  indent_level = line.index("def")
94
125
  method_lines << line
95
126
  current_line += 1
@@ -104,7 +135,7 @@ module EN14960
104
135
  line = lines[current_line]
105
136
  method_lines << line
106
137
 
107
- if line.strip == "end" && (line.index(/\S/) || 0) <= indent_level
138
+ if line.strip == "end" && indent_level && (line.index(/\S/) || 0) <= indent_level
108
139
  break
109
140
  end
110
141
 
@@ -114,6 +145,7 @@ module EN14960
114
145
  method_lines
115
146
  end
116
147
 
148
+ sig { params(source_code: String).returns(String) }
117
149
  private_class_method def self.strip_consistent_indentation(source_code)
118
150
  lines = source_code.split("\n")
119
151
 
@@ -1,51 +1,55 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "../constants"
4
6
 
5
7
  module EN14960
6
8
  module Validators
7
9
  module MaterialValidator
10
+ extend T::Sig
8
11
  extend self
9
12
 
13
+ sig { params(diameter_mm: Float).returns(T::Boolean) }
10
14
  def valid_rope_diameter?(diameter_mm)
11
15
  # EN 14960:2019 - Rope diameter range prevents finger entrapment while
12
16
  # ensuring adequate grip and structural strength
13
- return false if diameter_mm.nil?
14
17
 
15
18
  min_diameter = Constants::MATERIAL_STANDARDS[:rope][:min_diameter]
16
19
  max_diameter = Constants::MATERIAL_STANDARDS[:rope][:max_diameter]
17
20
  diameter_mm.between?(min_diameter, max_diameter)
18
21
  end
19
22
 
23
+ sig { returns(String) }
20
24
  def fabric_tensile_requirement
21
25
  fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
22
26
  "#{fabric_standards[:min_tensile_strength]} Newtons minimum"
23
27
  end
24
28
 
29
+ sig { returns(String) }
25
30
  def fabric_tear_requirement
26
31
  fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
27
32
  "#{fabric_standards[:min_tear_strength]} Newtons minimum"
28
33
  end
29
34
 
30
35
  # Additional validation methods
36
+ sig { params(strength_n: Float).returns(T::Boolean) }
31
37
  def valid_fabric_tensile_strength?(strength_n)
32
- return false if strength_n.nil?
33
38
  strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tensile_strength]
34
39
  end
35
40
 
41
+ sig { params(strength_n: Float).returns(T::Boolean) }
36
42
  def valid_fabric_tear_strength?(strength_n)
37
- return false if strength_n.nil?
38
43
  strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tear_strength]
39
44
  end
40
45
 
46
+ sig { params(strength_n: Float).returns(T::Boolean) }
41
47
  def valid_thread_tensile_strength?(strength_n)
42
- return false if strength_n.nil?
43
48
  strength_n >= Constants::MATERIAL_STANDARDS[:thread][:min_tensile_strength]
44
49
  end
45
50
 
51
+ sig { params(mesh_mm: Float, is_roof: T::Boolean).returns(T::Boolean) }
46
52
  def valid_netting_mesh?(mesh_mm, is_roof: false)
47
- return false if mesh_mm.nil?
48
-
49
53
  max_mesh = is_roof ?
50
54
  Constants::MATERIAL_STANDARDS[:netting][:max_roof_mesh] :
51
55
  Constants::MATERIAL_STANDARDS[:netting][:max_vertical_mesh]
@@ -1,10 +1,23 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
2
5
 
3
6
  module EN14960
4
7
  module Validators
5
8
  module PlayAreaValidator
9
+ extend T::Sig
6
10
  extend self
7
11
 
12
+ sig {
13
+ params(
14
+ unit_length: Float,
15
+ unit_width: Float,
16
+ play_area_length: Float,
17
+ play_area_width: Float,
18
+ negative_adjustment_area: Float
19
+ ).returns(T::Hash[Symbol, T.untyped])
20
+ }
8
21
  def validate(
9
22
  unit_length:,
10
23
  unit_width:,
@@ -14,17 +27,7 @@ module EN14960
14
27
  )
15
28
  errors = []
16
29
 
17
- if [
18
- unit_length,
19
- unit_width,
20
- play_area_length,
21
- play_area_width,
22
- negative_adjustment_area
23
- ].any?(&:nil?)
24
- errors << "All measurements must be provided"
25
- end
26
-
27
- return build_response(false, errors) unless errors.empty?
30
+ # All parameters are required, no nil checks needed
28
31
 
29
32
  unit_length = unit_length.to_f
30
33
  unit_width = unit_width.to_f
@@ -61,6 +64,7 @@ module EN14960
61
64
 
62
65
  private
63
66
 
67
+ sig { params(valid: T::Boolean, errors: T::Array[String], measurements: T::Hash[Symbol, Float]).returns(T::Hash[Symbol, T.untyped]) }
64
68
  def build_response(valid, errors, measurements = {})
65
69
  {
66
70
  valid: valid,
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  module EN14960
4
- VERSION = "0.2.3"
5
+ VERSION = "0.4.0"
5
6
  end
data/lib/en14960.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "en14960/version"
4
6
  require_relative "en14960/models/calculator_response"
5
7
  require_relative "en14960/constants"
@@ -13,15 +15,19 @@ require_relative "en14960/source_code"
13
15
  # EN14960 provides calculators and validators for BS EN 14960:2019
14
16
  # the safety standard for inflatable play equipment
15
17
  module EN14960
18
+ extend T::Sig
19
+
16
20
  class Error < StandardError; end
17
21
 
18
22
  # Public API methods for easy access to calculators
19
23
  class << self
24
+ extend T::Sig
20
25
  # Calculate required anchors for inflatable play equipment
21
26
  # @param length [Float] Length in meters
22
27
  # @param width [Float] Width in meters
23
28
  # @param height [Float] Height in meters
24
29
  # @return [CalculatorResponse] Response with anchor count and breakdown
30
+ sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
25
31
  def calculate_anchors(length:, width:, height:)
26
32
  Calculators::AnchorCalculator.calculate(length: length, width: width, height: height)
27
33
  end
@@ -30,6 +36,7 @@ module EN14960
30
36
  # @param platform_height [Float] Platform height in meters
31
37
  # @param has_stop_wall [Boolean] Whether a stop wall is fitted
32
38
  # @return [CalculatorResponse] Response with runout distance and breakdown
39
+ sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
33
40
  def calculate_slide_runout(platform_height, has_stop_wall: false)
34
41
  Calculators::SlideCalculator.calculate_required_runout(platform_height, has_stop_wall: has_stop_wall)
35
42
  end
@@ -39,6 +46,7 @@ module EN14960
39
46
  # @param user_height [Float] Maximum user height in meters
40
47
  # @param has_permanent_roof [Boolean] Whether unit has permanent roof
41
48
  # @return [CalculatorResponse] Response with wall height requirements
49
+ sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
42
50
  def calculate_wall_height(platform_height, user_height, has_permanent_roof = nil)
43
51
  Calculators::SlideCalculator.calculate_wall_height_requirements(
44
52
  platform_height,
@@ -53,7 +61,8 @@ module EN14960
53
61
  # @param max_user_height [Float, nil] Maximum allowed user height
54
62
  # @param negative_adjustment_area [Float] Area to subtract for obstacles
55
63
  # @return [CalculatorResponse] Response with capacity by user height
56
- def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0)
64
+ sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
65
+ def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0.0)
57
66
  Calculators::UserCapacityCalculator.calculate(
58
67
  length,
59
68
  width,
@@ -65,18 +74,21 @@ module EN14960
65
74
  # Check if rope diameter meets safety requirements
66
75
  # @param diameter_mm [Float] Rope diameter in millimeters
67
76
  # @return [Boolean] Whether diameter is within safe range
77
+ sig { params(diameter_mm: Float).returns(T::Boolean) }
68
78
  def valid_rope_diameter?(diameter_mm)
69
79
  Validators::MaterialValidator.valid_rope_diameter?(diameter_mm)
70
80
  end
71
81
 
72
82
  # Get height categories defined by EN 14960:2019
73
83
  # @return [Hash] Height categories with labels and requirements
84
+ sig { returns(T::Hash[Symbol, T.untyped]) }
74
85
  def height_categories
75
86
  Constants::HEIGHT_CATEGORIES
76
87
  end
77
88
 
78
89
  # Get material standards defined by EN 14960:2019
79
90
  # @return [Hash] Material requirements for fabrics, threads, ropes, and netting
91
+ sig { returns(T::Hash[Symbol, T.untyped]) }
80
92
  def material_standards
81
93
  Constants::MATERIAL_STANDARDS
82
94
  end
@@ -88,6 +100,15 @@ module EN14960
88
100
  # @param play_area_width [Float] Play area width
89
101
  # @param negative_adjustment_area [Float] Negative adjustment area
90
102
  # @return [Hash] Validation result with errors and measurements
103
+ sig {
104
+ params(
105
+ unit_length: Float,
106
+ unit_width: Float,
107
+ play_area_length: Float,
108
+ play_area_width: Float,
109
+ negative_adjustment_area: Float
110
+ ).returns(T::Hash[Symbol, T.untyped])
111
+ }
91
112
  def validate_play_area(
92
113
  unit_length:,
93
114
  unit_width:,
data/sorbet/config ADDED
@@ -0,0 +1,6 @@
1
+ --dir
2
+ .
3
+ --ignore
4
+ vendor/
5
+ --ignore
6
+ spec/
data/terragon-setup.sh ADDED
@@ -0,0 +1,34 @@
1
+ #\!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Terragon Labs Rails Setup Script
5
+ # Optimized for Ubuntu 24.04 - runs on every sandbox start
6
+
7
+ # Skip if already set up (optimization for repeated runs)
8
+ if [ -f ".terragon-setup-complete" ] && [ -z "${FORCE_SETUP:-}" ]; then
9
+ echo "Setup already complete. Run with FORCE_SETUP=1 to re-run."
10
+ exit 0
11
+ fi
12
+
13
+ echo "Installing Ruby and dependencies..."
14
+ sudo apt-get update -qq
15
+ sudo apt-get install -y ruby-full ruby-bundler build-essential libsqlite3-dev libyaml-dev imagemagick
16
+
17
+ echo "Installing bundler gem..."
18
+ sudo gem install bundler
19
+
20
+ # Setup ImageMagick symlinks if needed
21
+ for cmd in identify mogrify convert; do
22
+ if \! command -v $cmd &> /dev/null; then
23
+ BINARY=$(ls /usr/bin/${cmd}-* 2>/dev/null | grep -E "${cmd}-im[0-9]" | head -1)
24
+ if [ -n "$BINARY" ]; then
25
+ sudo ln -sf "$BINARY" "/usr/local/bin/$cmd" 2>/dev/null || true
26
+ fi
27
+ fi
28
+ done
29
+
30
+ # Mark setup as complete
31
+ touch .terragon-setup-complete
32
+
33
+ echo "Rails setup complete\!"
34
+
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: en14960
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chobble.com
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-12 00:00:00.000000000 Z
11
+ date: 2025-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sorbet-runtime
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,34 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0.21'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sorbet
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: tapioca
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.16'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.16'
83
125
  description: A Ruby gem providing calculators and validators for BS EN 14960:2019
84
126
  - the safety standard for inflatable play equipment. Includes calculations for anchoring
85
127
  requirements, slide safety, user capacity, and material specifications.
@@ -91,6 +133,7 @@ extra_rdoc_files: []
91
133
  files:
92
134
  - ".envrc"
93
135
  - ".rspec"
136
+ - ".terragon-setup-complete"
94
137
  - CHANGELOG.md
95
138
  - LICENSE
96
139
  - LICENSE.txt
@@ -99,6 +142,7 @@ files:
99
142
  - flake.lock
100
143
  - flake.nix
101
144
  - lib/en14960.rb
145
+ - lib/en14960/api.rb
102
146
  - lib/en14960/calculators/anchor_calculator.rb
103
147
  - lib/en14960/calculators/slide_calculator.rb
104
148
  - lib/en14960/calculators/user_capacity_calculator.rb
@@ -108,6 +152,8 @@ files:
108
152
  - lib/en14960/validators/material_validator.rb
109
153
  - lib/en14960/validators/play_area_validator.rb
110
154
  - lib/en14960/version.rb
155
+ - sorbet/config
156
+ - terragon-setup.sh
111
157
  homepage: https://github.com/chobbledotcom/en14960
112
158
  licenses:
113
159
  - AGPL-3.0-or-later