ocl 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/lib/ocl/core.rb +236 -0
- data/lib/ocl.rb +8 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4be0739c4acc7e4530cbc6ccd5b63e4050070b90de637017d29d598feac67f7f
|
|
4
|
+
data.tar.gz: de6f9bb78f5ef602cc66d65c4c05d06b9a25acb9a3fb049a3a38b0e7947a794f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5a8272e8983110c534ff752f03d7d5830afb8696b24db3af8d4d53c03109ed3becf0dc20ec5a3690dff65664fc0d080207bbebf0e5ec03cf4919943a2b407fa6
|
|
7
|
+
data.tar.gz: 5809cf6b6b646f05de920deb6efb7792fbda06889aff2dc1c1572a0bac030017e552ac6b017a095ce2007516d10c778853c7a988201267df3692030204f92ea4
|
data/lib/ocl/core.rb
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/ocl/core.rb
|
|
4
|
+
|
|
5
|
+
# Main OCL module providing invariant, precondition, postcondition, and derived attribute support.
|
|
6
|
+
module OCL
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
base.prepend InitializerHook
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Class-level methods to define OCL constraints.
|
|
13
|
+
module ClassMethods
|
|
14
|
+
def invariants
|
|
15
|
+
@invariants ||= []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def preconditions
|
|
19
|
+
@preconditions ||= Hash.new { |h, k| h[k] = [] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def postconditions
|
|
23
|
+
@postconditions ||= Hash.new { |h, k| h[k] = [] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def derived_properties
|
|
27
|
+
@derived_properties ||= {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Define an invariant constraint.
|
|
31
|
+
def inv(name, &block)
|
|
32
|
+
invariants << { name: name, block: block }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Define a precondition constraint for a method.
|
|
36
|
+
def pre(method_name, name, &block)
|
|
37
|
+
wrap_method(method_name)
|
|
38
|
+
preconditions[method_name] << { name: name, block: block }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Define a postcondition constraint for a method.
|
|
42
|
+
def post(method_name, name, &block)
|
|
43
|
+
wrap_method(method_name)
|
|
44
|
+
postconditions[method_name] << { name: name, block: block }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Define a derived (calculated) attribute.
|
|
48
|
+
def derived(name, &block)
|
|
49
|
+
derived_properties[name.to_sym] = block
|
|
50
|
+
|
|
51
|
+
define_method(name) do
|
|
52
|
+
context = Context.new(self)
|
|
53
|
+
self.class.derived_properties[name.to_sym].call(context)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Define an attribute with automatic invariant validation on assignment.
|
|
58
|
+
def attr_accessor_with_invariant(*names)
|
|
59
|
+
names.each do |name|
|
|
60
|
+
attr_reader name
|
|
61
|
+
|
|
62
|
+
define_method("#{name}=") do |value|
|
|
63
|
+
instance_variable_set("@#{name}", value)
|
|
64
|
+
validate_invariants!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Wrap an instance method to validate preconditions and postconditions.
|
|
72
|
+
def wrap_method(method_name)
|
|
73
|
+
@wrapped_methods ||= Set.new
|
|
74
|
+
return if @wrapped_methods.include?(method_name)
|
|
75
|
+
|
|
76
|
+
original = instance_method(method_name)
|
|
77
|
+
@wrapped_methods.add(method_name)
|
|
78
|
+
|
|
79
|
+
define_method(method_name) do |*args, &block|
|
|
80
|
+
validate_preconditions!(method_name, *args)
|
|
81
|
+
result = original.bind(self).call(*args, &block)
|
|
82
|
+
validate_postconditions!(method_name, result, *args)
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Internal hook to ensure superclass initialize is called properly.
|
|
89
|
+
module InitializerHook
|
|
90
|
+
def initialize(*args, &block)
|
|
91
|
+
super(*args, &block)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Raised when any constraint validation fails.
|
|
96
|
+
class ConstraintViolationError < StandardError
|
|
97
|
+
def initialize(errors)
|
|
98
|
+
super(errors.is_a?(Array) ? errors.join(', ') : errors)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Context passed to validation blocks, providing access to the target object and helpers.
|
|
103
|
+
class Context
|
|
104
|
+
attr_reader :object
|
|
105
|
+
|
|
106
|
+
def initialize(object)
|
|
107
|
+
@object = object
|
|
108
|
+
@errors = []
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def method_missing(name, *args, &block)
|
|
112
|
+
if args.empty?
|
|
113
|
+
object.public_send(name)
|
|
114
|
+
else
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create an expectation object for fluent assertions.
|
|
124
|
+
def expect(actual)
|
|
125
|
+
Expectation.new(actual, self)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Record an error message in the context.
|
|
129
|
+
def add_error(detail)
|
|
130
|
+
@errors << detail
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if all assertions passed.
|
|
134
|
+
def valid?
|
|
135
|
+
@errors.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Return accumulated error messages.
|
|
139
|
+
def error_messages
|
|
140
|
+
@errors
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Fluent assertions used within validation blocks.
|
|
144
|
+
class Expectation
|
|
145
|
+
def initialize(actual, context)
|
|
146
|
+
@actual = actual
|
|
147
|
+
@context = context
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def to_be(expected)
|
|
151
|
+
return if @actual == expected
|
|
152
|
+
|
|
153
|
+
@context.add_error("Expected #{@actual.inspect} to equal #{expected.inspect}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def to_not_be(expected)
|
|
157
|
+
return unless @actual == expected
|
|
158
|
+
|
|
159
|
+
@context.add_error("Expected #{@actual.inspect} to not equal #{expected.inspect}")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def to_be_positive
|
|
163
|
+
return if @actual.positive?
|
|
164
|
+
|
|
165
|
+
@context.add_error("Expected #{@actual.inspect} to be positive")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_be_greater_than(value)
|
|
169
|
+
return if @actual > value
|
|
170
|
+
|
|
171
|
+
@context.add_error("Expected #{@actual.inspect} to be greater than #{value.inspect}")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def to_be_greater_than_or_equal_to(value)
|
|
175
|
+
return if @actual >= value
|
|
176
|
+
|
|
177
|
+
@context.add_error("Expected #{@actual.inspect} to be greater than or equal to #{value.inspect}")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_be_less_than(value)
|
|
181
|
+
return if @actual < value
|
|
182
|
+
|
|
183
|
+
@context.add_error("Expected #{@actual.inspect} to be less than #{value.inspect}")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def to_be_less_than_or_equal_to(value)
|
|
187
|
+
return if @actual <= value
|
|
188
|
+
|
|
189
|
+
@context.add_error("Expected #{@actual.inspect} to be less than or equal to #{value.inspect}")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Validate all defined invariants.
|
|
195
|
+
def validate_invariants!
|
|
196
|
+
errors = []
|
|
197
|
+
|
|
198
|
+
self.class.invariants.each do |inv|
|
|
199
|
+
context = Context.new(self)
|
|
200
|
+
inv[:block].call(context)
|
|
201
|
+
errors << "Invariant '#{inv[:name]}' violated: " + context.error_messages.join(', ') unless context.valid?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
raise ConstraintViolationError, errors unless errors.empty?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Validate preconditions before method execution.
|
|
208
|
+
def validate_preconditions!(method_name, *args)
|
|
209
|
+
errors = []
|
|
210
|
+
|
|
211
|
+
self.class.preconditions[method_name].each do |pre|
|
|
212
|
+
context = Context.new(self)
|
|
213
|
+
pre[:block].call(context, *args)
|
|
214
|
+
unless context.valid?
|
|
215
|
+
errors << "Precondition '#{pre[:name]}' for #{method_name} violated: " + context.error_messages.join(', ')
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
raise ConstraintViolationError, errors unless errors.empty?
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Validate postconditions after method execution.
|
|
223
|
+
def validate_postconditions!(method_name, result, *args)
|
|
224
|
+
errors = []
|
|
225
|
+
|
|
226
|
+
self.class.postconditions[method_name].each do |post|
|
|
227
|
+
context = Context.new(self)
|
|
228
|
+
post[:block].call(context, result, *args)
|
|
229
|
+
unless context.valid?
|
|
230
|
+
errors << "Postcondition '#{post[:name]}' for #{method_name} violated: " + context.error_messages.join(', ')
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
raise ConstraintViolationError, errors unless errors.empty?
|
|
235
|
+
end
|
|
236
|
+
end
|
data/lib/ocl.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ocl
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Takeshi Kakeda
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.12'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.12'
|
|
26
|
+
description: Supports invariant, precondition, postcondition, and derived attributes
|
|
27
|
+
using a Ruby DSL.
|
|
28
|
+
email:
|
|
29
|
+
- takeshi@giantech.jp
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/ocl.rb
|
|
35
|
+
- lib/ocl/core.rb
|
|
36
|
+
homepage: https://github.com/yourname/ocl
|
|
37
|
+
licenses:
|
|
38
|
+
- MIT
|
|
39
|
+
metadata: {}
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.2'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.6.8
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: A minimal Object Constraint Language (OCL) engine for Ruby.
|
|
57
|
+
test_files: []
|