calc_kit 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/Rakefile +12 -0
- data/calc_kit.gemspec +37 -0
- data/lib/calc_kit/base.rb +61 -0
- data/lib/calc_kit/configuration.rb +21 -0
- data/lib/calc_kit/dsl.rb +99 -0
- data/lib/calc_kit/engine.rb +36 -0
- data/lib/calc_kit/input_definition.rb +63 -0
- data/lib/calc_kit/output_definition.rb +35 -0
- data/lib/calc_kit/rails/controller_helpers.rb +76 -0
- data/lib/calc_kit/rails/model.rb +48 -0
- data/lib/calc_kit/rails/view_helpers.rb +66 -0
- data/lib/calc_kit/registry.rb +37 -0
- data/lib/calc_kit/version.rb +5 -0
- data/lib/calc_kit.rb +42 -0
- data/lib/generators/calc_kit/calculator_generator.rb +39 -0
- data/lib/generators/calc_kit/install_generator.rb +68 -0
- data/lib/generators/calc_kit/templates/application_calculator.rb.tt +13 -0
- data/lib/generators/calc_kit/templates/calculation.rb.tt +9 -0
- data/lib/generators/calc_kit/templates/calculator.rb.tt +27 -0
- data/lib/generators/calc_kit/templates/calculator_test.rb.tt +32 -0
- data/lib/generators/calc_kit/templates/create_calculations.rb.tt +22 -0
- data/lib/generators/calc_kit/templates/initializer.rb.tt +27 -0
- data/lib/generators/calc_kit/templates/views/_form.html.erb +30 -0
- data/lib/generators/calc_kit/templates/views/_result.html.erb +17 -0
- data/lib/generators/calc_kit/templates/views/_saved_calculations.html.erb +36 -0
- data/lib/generators/calc_kit/templates/views/create.turbo_stream.erb +3 -0
- data/lib/generators/calc_kit/templates/views/index.html.erb +11 -0
- data/lib/generators/calc_kit/templates/views/show.html.erb +13 -0
- metadata +132 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 89540dec57ad5480d42f475262ad432f1a275b7d0519287c3e5e2092977d4399
|
|
4
|
+
data.tar.gz: e3ef5386ea201bc2e1d6d7a1bd267d569f78785a8ed6920c3d63fb8963601175
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ea9321a56fe0aa4b820ece61a754302a389831d66765c4dd4cbb6b1aa279f37d2d0a2b55553e67c15efbf1139539615be19aefd6a56bd45870a0daa3944b3ef
|
|
7
|
+
data.tar.gz: b99c06861ebb054cb65876689130a22f7a0818aa5eca7db59a89e6a955cc87ff7d49ebb589c23b84f5d3a2c0b412235894832ef3349d483f43ffe63629fd0a9b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-06-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Declarative DSL for defining calculator inputs and outputs
|
|
13
|
+
- Input types: string, integer, decimal, date, select, boolean
|
|
14
|
+
- Output types: string, integer, decimal, date, currency, percentage
|
|
15
|
+
- ActiveModel validations (required, min, max)
|
|
16
|
+
- Calculator registry for lookup by slug
|
|
17
|
+
- Rails engine with controller helpers and view helpers
|
|
18
|
+
- Generators for install, calculator scaffolding
|
|
19
|
+
- Optional persistence via Calculation model concern
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tyler Schneider
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# CalcKit
|
|
2
|
+
|
|
3
|
+
A Ruby gem providing a declarative DSL for building calculators with automatic form generation, validation, and optional persistence. Works standalone with ActiveModel or as a Rails engine.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "calc_kit"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run the install generator:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rails g calc_kit:install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
- `--model` - Generate a Calculation model for persistence
|
|
21
|
+
- `--views` - Copy default views to your app
|
|
22
|
+
- `--scope=current_account` - Configure multi-tenancy scope
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Defining a Calculator
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# app/calculators/price_calculator.rb
|
|
30
|
+
class PriceCalculator < ApplicationCalculator
|
|
31
|
+
calculator_name "Price Calculator"
|
|
32
|
+
calculator_slug :price
|
|
33
|
+
version "1.0"
|
|
34
|
+
|
|
35
|
+
input :price, :decimal, label: "Unit Price", required: true, min: 0.01
|
|
36
|
+
input :quantity, :integer, label: "Quantity", required: true, min: 1
|
|
37
|
+
input :discount, :decimal, label: "Discount %", default: 0, min: 0, max: 100
|
|
38
|
+
|
|
39
|
+
output :subtotal, :decimal
|
|
40
|
+
output :total, :decimal
|
|
41
|
+
|
|
42
|
+
def calculate
|
|
43
|
+
sub = price * quantity
|
|
44
|
+
disc = sub * (discount / 100.0)
|
|
45
|
+
{
|
|
46
|
+
subtotal: sub,
|
|
47
|
+
total: sub - disc
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
CalcKit.register(PriceCalculator)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Input Types
|
|
56
|
+
|
|
57
|
+
- `:string` - Text input
|
|
58
|
+
- `:integer` - Whole number input
|
|
59
|
+
- `:decimal` - Decimal number input
|
|
60
|
+
- `:date` - Date picker
|
|
61
|
+
- `:select` - Dropdown (use `options:` to provide choices)
|
|
62
|
+
- `:boolean` - Checkbox
|
|
63
|
+
|
|
64
|
+
### Input Options
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
input :name, :type,
|
|
68
|
+
label: "Display Label", # Form label
|
|
69
|
+
required: true, # Adds presence validation
|
|
70
|
+
min: 0, # Minimum value (numeric)
|
|
71
|
+
max: 100, # Maximum value (numeric)
|
|
72
|
+
step: 0.01, # Step increment (decimal)
|
|
73
|
+
default: 10, # Default value (can be a proc)
|
|
74
|
+
placeholder: "Enter value", # Placeholder text
|
|
75
|
+
hint: "Help text", # Help text below input
|
|
76
|
+
options: [["Label", "value"], ...] # For :select type
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Output Types
|
|
80
|
+
|
|
81
|
+
- `:string` - Plain text
|
|
82
|
+
- `:integer` - Formatted integer
|
|
83
|
+
- `:decimal` - Formatted decimal (2 places)
|
|
84
|
+
- `:date` - Formatted date
|
|
85
|
+
- `:currency` - Currency formatted
|
|
86
|
+
- `:percentage` - Percentage formatted
|
|
87
|
+
|
|
88
|
+
### Running Calculations
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
calc = PriceCalculator.new(price: 10, quantity: 5, discount: 10)
|
|
92
|
+
|
|
93
|
+
if calc.valid?
|
|
94
|
+
result = calc.run
|
|
95
|
+
# => { subtotal: 50.0, total: 45.0 }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Or use run! to raise on validation errors
|
|
99
|
+
result = calc.run!
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Controller Integration
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class CalculatorsController < ApplicationController
|
|
106
|
+
include CalcKit::ControllerHelpers
|
|
107
|
+
|
|
108
|
+
def show
|
|
109
|
+
@calculator_class = find_calculator_class(params[:slug])
|
|
110
|
+
@calculator = build_calculator(@calculator_class)
|
|
111
|
+
@saved_calculations = load_saved_calculations(@calculator_class)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create
|
|
115
|
+
@calculator_class = find_calculator_class(params[:slug])
|
|
116
|
+
@calculator = build_calculator(@calculator_class)
|
|
117
|
+
@result = @calculator.run
|
|
118
|
+
|
|
119
|
+
if @result
|
|
120
|
+
save_calculation(@calculator, @result) if params[:save].present?
|
|
121
|
+
# ...
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### View Helpers
|
|
128
|
+
|
|
129
|
+
```erb
|
|
130
|
+
<%# Resolve callable defaults %>
|
|
131
|
+
<%= calc_kit_resolve_default(input.default) %>
|
|
132
|
+
|
|
133
|
+
<%# Format output values %>
|
|
134
|
+
<%= calc_kit_format_output(result[:total], :currency) %>
|
|
135
|
+
|
|
136
|
+
<%# CSS classes %>
|
|
137
|
+
<%= calc_kit_input_class(calculator, input) %>
|
|
138
|
+
<%= calc_kit_label_class %>
|
|
139
|
+
<%= calc_kit_error_class %>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# config/initializers/calc_kit.rb
|
|
146
|
+
CalcKit.configure do |config|
|
|
147
|
+
# Path to calculator classes
|
|
148
|
+
config.calculators_path = "app/calculators"
|
|
149
|
+
|
|
150
|
+
# Auto-register calculators on load
|
|
151
|
+
config.auto_register = true
|
|
152
|
+
|
|
153
|
+
# Multi-tenancy scope method
|
|
154
|
+
config.scope_method = :current_account
|
|
155
|
+
|
|
156
|
+
# Enable Turbo Streams
|
|
157
|
+
config.enable_turbo_streams = true
|
|
158
|
+
|
|
159
|
+
# Enable calculation persistence
|
|
160
|
+
config.save_calculations = true
|
|
161
|
+
|
|
162
|
+
# Default CSS classes
|
|
163
|
+
config.default_form_classes = {
|
|
164
|
+
input: "form-control",
|
|
165
|
+
label: "block text-sm font-medium mb-1",
|
|
166
|
+
error: "text-red-500 text-sm mt-1",
|
|
167
|
+
submit: "btn btn-primary"
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Generators
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Install calc_kit
|
|
176
|
+
rails g calc_kit:install
|
|
177
|
+
|
|
178
|
+
# Generate a new calculator
|
|
179
|
+
rails g calc_kit:calculator shipping
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Roadmap
|
|
183
|
+
|
|
184
|
+
Potential future enhancements:
|
|
185
|
+
|
|
186
|
+
### Input Enhancements
|
|
187
|
+
- [ ] Input groups for organizing related fields
|
|
188
|
+
- [ ] Conditional inputs (show/hide based on other values)
|
|
189
|
+
- [ ] Custom validators beyond min/max
|
|
190
|
+
- [ ] More types: `:textarea`, `:radio`, `:checkbox_group`, `:range`
|
|
191
|
+
|
|
192
|
+
### Output Enhancements
|
|
193
|
+
- [ ] Custom formatters
|
|
194
|
+
- [ ] Conditional outputs (only show if value present)
|
|
195
|
+
- [ ] Output groups/sections
|
|
196
|
+
|
|
197
|
+
### Calculator Features
|
|
198
|
+
- [ ] Calculator descriptions for index pages
|
|
199
|
+
- [ ] Categories/tags for organizing calculators
|
|
200
|
+
- [ ] Comparison mode (side-by-side with different inputs)
|
|
201
|
+
- [ ] Calculator dependencies (one calculator feeds into another)
|
|
202
|
+
|
|
203
|
+
### Export & Sharing
|
|
204
|
+
- [ ] PDF export of results
|
|
205
|
+
- [ ] CSV export for saved calculations
|
|
206
|
+
- [ ] Shareable result links
|
|
207
|
+
- [ ] Embeddable calculator widgets
|
|
208
|
+
|
|
209
|
+
### Developer Experience
|
|
210
|
+
- [ ] Controller scaffold generator
|
|
211
|
+
- [ ] API endpoint generator
|
|
212
|
+
- [ ] JavaScript client for real-time calculation preview
|
|
213
|
+
- [ ] Form builder integration (SimpleForm, Formtastic)
|
|
214
|
+
|
|
215
|
+
### Testing
|
|
216
|
+
- [ ] Calculator test helpers
|
|
217
|
+
- [ ] Shared examples for common patterns
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT License
|
data/Rakefile
ADDED
data/calc_kit.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/calc_kit/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "calc_kit"
|
|
7
|
+
spec.version = CalcKit::VERSION
|
|
8
|
+
spec.authors = ["Tyler Schneider"]
|
|
9
|
+
spec.email = ["tylercschneider@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A DSL for building calculators with automatic form generation"
|
|
12
|
+
spec.description = "CalcKit provides a declarative DSL for defining calculators with inputs, outputs, validation, and optional persistence. Works standalone or as a Rails engine."
|
|
13
|
+
spec.homepage = "https://github.com/tylercschneider/calc_kit"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
|
25
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor docs/ Gemfile CLAUDE.md])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_dependency "activemodel", ">= 7.0"
|
|
33
|
+
|
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
36
|
+
spec.add_development_dependency "railties", ">= 7.0"
|
|
37
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module CalcKit
|
|
6
|
+
# Base class for all calculators with a declarative DSL
|
|
7
|
+
#
|
|
8
|
+
# Responsibilities (Single Responsibility):
|
|
9
|
+
# - Defines DSL for declaring calculator metadata, inputs, and outputs
|
|
10
|
+
# - Provides validation through ActiveModel
|
|
11
|
+
#
|
|
12
|
+
# Open/Closed: New calculators extend this class without modifying it
|
|
13
|
+
# Liskov Substitution: All subclasses can be used interchangeably via #run
|
|
14
|
+
# Interface Segregation: InputDefinition and OutputDefinition are separate classes
|
|
15
|
+
# Dependency Inversion: Uses ActiveModel abstractions, not concrete implementations
|
|
16
|
+
#
|
|
17
|
+
class Base
|
|
18
|
+
include ActiveModel::Model
|
|
19
|
+
include ActiveModel::Attributes
|
|
20
|
+
include DSL
|
|
21
|
+
|
|
22
|
+
# Run calculation if valid, return nil if invalid
|
|
23
|
+
def run
|
|
24
|
+
return nil unless valid?
|
|
25
|
+
calculate
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Run calculation, raise if invalid
|
|
29
|
+
def run!
|
|
30
|
+
raise ActiveModel::ValidationError, self unless valid?
|
|
31
|
+
calculate
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Subclasses must implement this
|
|
35
|
+
def calculate
|
|
36
|
+
raise NotImplementedError, "#{self.class.name} must implement #calculate"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns input values as a hash
|
|
40
|
+
def input_values
|
|
41
|
+
self.class.inputs.each_with_object({}) do |input, hash|
|
|
42
|
+
hash[input.name] = send(input.name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the calculator name
|
|
47
|
+
def calculator_name
|
|
48
|
+
self.class.calculator_name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the calculator slug
|
|
52
|
+
def calculator_slug
|
|
53
|
+
self.class.calculator_slug
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the calculator version
|
|
57
|
+
def version
|
|
58
|
+
self.class.version
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :calculators_path,
|
|
6
|
+
:auto_register,
|
|
7
|
+
:scope_method,
|
|
8
|
+
:enable_turbo_streams,
|
|
9
|
+
:save_calculations,
|
|
10
|
+
:warn_on_version_mismatch
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@calculators_path = "app/calculators"
|
|
14
|
+
@auto_register = true
|
|
15
|
+
@scope_method = nil
|
|
16
|
+
@enable_turbo_streams = true
|
|
17
|
+
@save_calculations = true
|
|
18
|
+
@warn_on_version_mismatch = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/calc_kit/dsl.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# DSL module providing class-level macros for calculator definitions
|
|
5
|
+
module DSL
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
base.class_eval do
|
|
9
|
+
class_attribute :_calculator_name, :_calculator_slug, :_calculator_version,
|
|
10
|
+
:_inputs, :_outputs, instance_writer: false
|
|
11
|
+
|
|
12
|
+
self._inputs = []
|
|
13
|
+
self._outputs = []
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
def calculator_name(name = nil)
|
|
19
|
+
if name
|
|
20
|
+
self._calculator_name = name
|
|
21
|
+
else
|
|
22
|
+
_calculator_name
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def calculator_slug(slug = nil)
|
|
27
|
+
if slug
|
|
28
|
+
self._calculator_slug = slug.to_sym
|
|
29
|
+
else
|
|
30
|
+
_calculator_slug
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def version(v = nil)
|
|
35
|
+
if v
|
|
36
|
+
self._calculator_version = v
|
|
37
|
+
else
|
|
38
|
+
_calculator_version
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def input(name, type, **options)
|
|
43
|
+
self._inputs = _inputs + [InputDefinition.new(name, type, options)]
|
|
44
|
+
|
|
45
|
+
# Define attribute using ActiveModel::Attributes
|
|
46
|
+
attribute name, type_for_attribute(type)
|
|
47
|
+
|
|
48
|
+
# Add validations based on options
|
|
49
|
+
validates name, presence: true if options[:required]
|
|
50
|
+
|
|
51
|
+
if options[:min]
|
|
52
|
+
validates name, numericality: { greater_than_or_equal_to: options[:min] }, allow_blank: true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if options[:max]
|
|
56
|
+
validates name, numericality: { less_than_or_equal_to: options[:max] }, allow_blank: true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def output(name, type, **options)
|
|
61
|
+
self._outputs = _outputs + [OutputDefinition.new(name, type, options)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inputs
|
|
65
|
+
_inputs
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def outputs
|
|
69
|
+
_outputs
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def input_for(name)
|
|
73
|
+
_inputs.find { |i| i.name == name }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def output_for(name)
|
|
77
|
+
_outputs.find { |o| o.name == name }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
VALID_INPUT_TYPES = %i[string integer decimal date boolean select].freeze
|
|
83
|
+
|
|
84
|
+
def type_for_attribute(type)
|
|
85
|
+
unless VALID_INPUT_TYPES.include?(type)
|
|
86
|
+
raise ArgumentError, "Unknown input type: #{type.inspect}. Valid types: #{VALID_INPUT_TYPES.join(", ")}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
case type
|
|
90
|
+
when :date then :date
|
|
91
|
+
when :integer then :integer
|
|
92
|
+
when :decimal then :decimal
|
|
93
|
+
when :boolean then :boolean
|
|
94
|
+
when :string, :select then :string
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace CalcKit
|
|
6
|
+
|
|
7
|
+
config.autoload_paths << root.join("lib")
|
|
8
|
+
|
|
9
|
+
initializer "calc_kit.eager_load_calculators" do |app|
|
|
10
|
+
app.config.to_prepare do
|
|
11
|
+
if CalcKit.configuration.auto_register
|
|
12
|
+
calculators_path = ::Rails.root.join(CalcKit.configuration.calculators_path)
|
|
13
|
+
if calculators_path.exist?
|
|
14
|
+
::Rails.autoloaders.main.eager_load_dir(calculators_path)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "calc_kit.view_helpers" do
|
|
21
|
+
ActiveSupport.on_load(:action_view) do
|
|
22
|
+
include CalcKit::ViewHelpers
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer "calc_kit.controller_helpers" do
|
|
27
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
28
|
+
include CalcKit::ControllerHelpers
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require_relative "rails/controller_helpers"
|
|
35
|
+
require_relative "rails/view_helpers"
|
|
36
|
+
require_relative "rails/model"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# Value object for input field definitions
|
|
5
|
+
class InputDefinition
|
|
6
|
+
attr_reader :name, :type, :options
|
|
7
|
+
|
|
8
|
+
def initialize(name, type, options = {})
|
|
9
|
+
@name = name
|
|
10
|
+
@type = type
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def label
|
|
15
|
+
options[:label] || name.to_s.tr("_", " ").capitalize
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def required?
|
|
19
|
+
options[:required] == true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default
|
|
23
|
+
options[:default]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def placeholder
|
|
27
|
+
options[:placeholder]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def min
|
|
31
|
+
options[:min]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def max
|
|
35
|
+
options[:max]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def step
|
|
39
|
+
options[:step]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def options_for_select
|
|
43
|
+
options[:options]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def hint
|
|
47
|
+
options[:hint]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ==(other)
|
|
51
|
+
other.is_a?(InputDefinition) &&
|
|
52
|
+
name == other.name &&
|
|
53
|
+
type == other.type &&
|
|
54
|
+
options == other.options
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias_method :eql?, :==
|
|
58
|
+
|
|
59
|
+
def hash
|
|
60
|
+
[name, type, options].hash
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# Value object for output field definitions
|
|
5
|
+
class OutputDefinition
|
|
6
|
+
attr_reader :name, :type, :options
|
|
7
|
+
|
|
8
|
+
def initialize(name, type, options = {})
|
|
9
|
+
@name = name
|
|
10
|
+
@type = type
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def label
|
|
15
|
+
options[:label] || name.to_s.tr("_", " ").capitalize
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format
|
|
19
|
+
options[:format]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(OutputDefinition) &&
|
|
24
|
+
name == other.name &&
|
|
25
|
+
type == other.type &&
|
|
26
|
+
options == other.options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias_method :eql?, :==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
[name, type, options].hash
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|