active_interface 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/active_interface/base.rb +61 -0
- data/lib/active_interface/contract.rb +36 -0
- data/lib/active_interface/interface_error.rb +14 -0
- data/lib/active_interface/verify.rb +32 -0
- data/lib/active_interface/version.rb +5 -0
- data/lib/active_interface.rb +13 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 42dde65b9a4587eb6f8523824689d6777401ed5c870b5dc6695a49bd41e68332
|
4
|
+
data.tar.gz: f30a127da41095e8d13808acb4ac339ec9e0efa2a0d7f8da16a975bd29e7bc40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eaba98da8342d0f3f5b4c3e26079809be1166ff2473a3fb17e1fafa0ffc935d0ea79c8ddb47482b45ffdecd5ab6343029ea562ee7141e339956f57b8f83af529
|
7
|
+
data.tar.gz: 3d9a69a81a23576379b96f25544c2b12d39bcef516e2e35213574049cb523c0d8e385e6c4a5b1b4a3d94ce31d792b778ca2328b83a6ae32c71b90bb990814b61
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
active_interface (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
coderay (1.1.3)
|
10
|
+
diff-lcs (1.5.0)
|
11
|
+
method_source (1.0.0)
|
12
|
+
pry (0.14.2)
|
13
|
+
coderay (~> 1.1)
|
14
|
+
method_source (~> 1.0)
|
15
|
+
rake (13.0.6)
|
16
|
+
rspec (3.12.0)
|
17
|
+
rspec-core (~> 3.12.0)
|
18
|
+
rspec-expectations (~> 3.12.0)
|
19
|
+
rspec-mocks (~> 3.12.0)
|
20
|
+
rspec-core (3.12.1)
|
21
|
+
rspec-support (~> 3.12.0)
|
22
|
+
rspec-expectations (3.12.2)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.12.0)
|
25
|
+
rspec-mocks (3.12.3)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.12.0)
|
28
|
+
rspec-support (3.12.0)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
arm64-darwin-21
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
active_interface!
|
35
|
+
pry (~> 0.14)
|
36
|
+
rake (~> 13.0)
|
37
|
+
rspec (~> 3.0)
|
38
|
+
|
39
|
+
BUNDLED WITH
|
40
|
+
2.3.8
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Russell Jennings
|
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,122 @@
|
|
1
|
+
# active_interface
|
2
|
+
|
3
|
+
ActiveInterface is a Ruby library for defining OOP interfaces in ruby
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
Add to your Gemfile
|
8
|
+
|
9
|
+
```
|
10
|
+
gem 'active_interface'
|
11
|
+
```
|
12
|
+
|
13
|
+
Then Bundle install. Now create a directory like `app/interfaces` and put your first interface there. Any methods defined (or attributes specified in `REQUIRED_ATTRIBUTES`) will be enforced on the class the interface is appended to.
|
14
|
+
|
15
|
+
```
|
16
|
+
module ExampleInterface
|
17
|
+
extend ActiveInterface::Base
|
18
|
+
|
19
|
+
REQUIRED_ATTRIBUTES = %i[size count count=].freeze
|
20
|
+
|
21
|
+
def example
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
Make sure any methods defined either call `super` or `interface_contract` (more info below) in order for the underlying method to be executed.
|
28
|
+
|
29
|
+
for any class that you want to apply this interface to, append it after the definition. This ensures all the methods are defined by the class, and that when the Interface is appended that it will sit in front of the method calls and act as a pass through.
|
30
|
+
|
31
|
+
```
|
32
|
+
class MyClass
|
33
|
+
...
|
34
|
+
end
|
35
|
+
MyClass.append ExampleInterface
|
36
|
+
```
|
37
|
+
|
38
|
+
## What's an OOP interface?
|
39
|
+
|
40
|
+
An interfaces allows developers to define an abstract collection of methods and attributes that must be implemented by a class. This allows developers to code against an abstract interface (that many classes could implement) instead of just a specific classes implementation.
|
41
|
+
|
42
|
+
Given the following class
|
43
|
+
```
|
44
|
+
class User
|
45
|
+
|
46
|
+
attr_accessor :first_name, :last_name
|
47
|
+
|
48
|
+
def full_name(seperator)
|
49
|
+
first_name + seperator + last_name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
if we also want a `Contact` or `Admin` to have the same behavior, we might reach for creating a common `Person` class. But what if each of our Classes already inherit from another class? We could extact everything into a `PersonModule`. But if we also have a `Product` class, or a `Car` class with the same behavior but their own implementations, how can we allow them to be wildly different and yet similar enough to treat the same in certain circumstances?
|
55
|
+
|
56
|
+
Enter Active Interface!
|
57
|
+
|
58
|
+
```
|
59
|
+
module NameInterface
|
60
|
+
|
61
|
+
extend ActiveInterface::Base
|
62
|
+
|
63
|
+
REQUIRED_ATTRIBUTES = %i[first_name last_name].freeze
|
64
|
+
|
65
|
+
def full_name(seperator)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
User.append NameInterface
|
72
|
+
```
|
73
|
+
|
74
|
+
Once we append `User` with `NameInterface`, we gain the following:
|
75
|
+
- Will raise exception if any of the required attributes are not defined
|
76
|
+
- Will raise exception if the expected methods are not defined or have different method signatures
|
77
|
+
- the interface sits in front of every call to the methods
|
78
|
+
- we can ask `User.has_interface?(NameInterface) => true` to develop our code against.
|
79
|
+
|
80
|
+
## What's an interface contract?
|
81
|
+
|
82
|
+
Once appended, Active Interface will ensure that certain methods/signatures and attributes are present at initialization. However, as a ruby is a dynamically typed language, it can be ambiguious what the expected inputs and outputs are for a method or how flexible they are. As a developer that must rely on an interface created by another team, how can you be sure you'll get the expected return values, or that you know what the expected inputs are?
|
83
|
+
|
84
|
+
Enter Interface Contracts with Active Interface!
|
85
|
+
|
86
|
+
```
|
87
|
+
module NameInterface
|
88
|
+
|
89
|
+
include ActiveInterface::Base
|
90
|
+
|
91
|
+
REQUIRED_ATTRIBUTES = %i[first_name last_name].freeze
|
92
|
+
|
93
|
+
def full_name(seperator)
|
94
|
+
interface_contract(binding) do |c|
|
95
|
+
c.enforce_input :seperator, kind_of: String, length: 1..4
|
96
|
+
c.enforce_output kind_of: String
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
User.append NameInterface
|
102
|
+
```
|
103
|
+
|
104
|
+
With the above Interface, every call to `#full_name` will be enforced to adheres to the interface contract. This carries a number of benefits:
|
105
|
+
- Documentation about what the expected parameters and return values are for the Interface
|
106
|
+
- method level validations of input and output values abstracted from implementation
|
107
|
+
- raises `InterfaceError` for any incorrect parameters per the contract
|
108
|
+
- raises `InterfaceError` if the output does not conform to the contract
|
109
|
+
- Ensures developers can implement interfaces and code against them reliably
|
110
|
+
- the results of `super` are only called once all the inputs have been enforced, or at the end of the block.
|
111
|
+
- If there are no exceptions raised, the block returns the result of `super`
|
112
|
+
|
113
|
+
## Other Active Interface uses
|
114
|
+
Because of the runtime capabilities, Active Interface can be used for more than just API conformity. Some additional uses could include:
|
115
|
+
|
116
|
+
- Logging request/response of method calls.
|
117
|
+
- Transforming inputs or outputs (such as always calling `.to_s`)
|
118
|
+
- Caching
|
119
|
+
- Running history of previous inputs and outputs
|
120
|
+
- And more!
|
121
|
+
|
122
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "active_interface"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
class Module
|
3
|
+
alias_method :has_interface?, :<
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
module ActiveInterface
|
8
|
+
module Base
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def prepended(klass)
|
12
|
+
messages = []
|
13
|
+
ensure_methods_implemented(klass, messages)
|
14
|
+
ensure_method_signatures(klass, messages)
|
15
|
+
ensure_attributes_defined(klass, messages)
|
16
|
+
|
17
|
+
if messages.size > 0
|
18
|
+
raise "#{messages.size} errors verifying #{klass} conforms to #{self} \n" + messages.join("\n")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def ensure_attributes_defined(klass, messages)
|
25
|
+
missing_attributes = klass::REQUIRED_ATTRIBUTES - klass.public_instance_methods(false)
|
26
|
+
if (missing_attributes.size > 0)
|
27
|
+
messages << "#{klass} is missing attributes #{missing_attributes.join(', ')}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def ensure_methods_implemented(klass, messages)
|
32
|
+
missing_methods = public_instance_methods(false) - klass.public_instance_methods(false)
|
33
|
+
if (missing_methods.size > 0)
|
34
|
+
messages << "#{klass} is missing implementation(s) for ##{missing_methods.join(', #')}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def ensure_method_signatures(klass, messages)
|
39
|
+
public_instance_methods(false).each do |method_name|
|
40
|
+
interface_method = instance_method(method_name)
|
41
|
+
class_method = klass.instance_method(method_name).super_method
|
42
|
+
next if class_method.nil?
|
43
|
+
interface_params = interface_method.parameters.map(&:last)
|
44
|
+
class_params = class_method.parameters.map(&:last)
|
45
|
+
|
46
|
+
unless interface_params == class_params
|
47
|
+
messages << "method signature for #{method_name} should be #{interface_params} but was #{class_params}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def interface_contract(_binding)
|
54
|
+
ActiveInterface::Contract.new(_binding).call
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.included(klass)
|
58
|
+
klass.extend(ClassMethods)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class ActiveInterface::Contract
|
2
|
+
|
3
|
+
def initialize(_binding, _output = nil)
|
4
|
+
@_binding = _binding
|
5
|
+
@output = _output
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(&block)
|
9
|
+
result = block.call self
|
10
|
+
output
|
11
|
+
end
|
12
|
+
|
13
|
+
def enforce_input(value_name, options)
|
14
|
+
callee = @_binding.eval("__method__")
|
15
|
+
interface = @_binding.eval("self").method(callee).owner
|
16
|
+
value = @_binding.eval(value_name.to_s)
|
17
|
+
verify(value_name, interface, value, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def enforce_output(options)
|
22
|
+
callee = @_binding.eval("__method__")
|
23
|
+
interface = @_binding.eval("self").method(callee).owner
|
24
|
+
verify(:output, interface, output, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def output
|
28
|
+
@output ||= @_binding.eval("super")
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify(attribute, interface, value, options = {})
|
32
|
+
Verify.call(attribute, interface, value, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class ActiveInterface::InterfaceError < StandardError
|
2
|
+
|
3
|
+
def initialize(errors, interface, klass, method_name: )
|
4
|
+
@interface = interface
|
5
|
+
@errors = errors
|
6
|
+
@klass = klass
|
7
|
+
@method_name = method_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"Violation of #{@interface} in #{@klass}##{@method_name}: #{@errors}"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Verify
|
2
|
+
|
3
|
+
def call(attribute, interface, value, options = {})
|
4
|
+
errors = []
|
5
|
+
if options[:presence] && value.nil?
|
6
|
+
errors << "#{attribute} can't be blank"
|
7
|
+
end
|
8
|
+
|
9
|
+
if options[:range] && !options[:range].include?(value)
|
10
|
+
errors << "#{attribute} is not within the range #{options[:in]}"
|
11
|
+
end
|
12
|
+
|
13
|
+
if options[:length] && !options[:length].include?(value.length)
|
14
|
+
errors << "#{attribute} must be between #{options[:length]} characters long"
|
15
|
+
end
|
16
|
+
|
17
|
+
if options[:regex] && !(options[:regex] =~ value)
|
18
|
+
errors << "#{attribute} is invalid"
|
19
|
+
end
|
20
|
+
|
21
|
+
if options[:kind_of] && !Array(options[:kind_of]).include?(value.class)
|
22
|
+
errors << "#{attribute} must be a kind of #{options[:kind_of]}"
|
23
|
+
end
|
24
|
+
if errors.empty?
|
25
|
+
nil
|
26
|
+
else
|
27
|
+
klass = @_binding.eval("self").class
|
28
|
+
meth = @_binding.eval("__method__")
|
29
|
+
raise(InterfaceError.new(errors, interface, klass, method_name: meth))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "active_interface/version"
|
4
|
+
|
5
|
+
require_relative "active_interface/base"
|
6
|
+
require_relative "active_interface/contract"
|
7
|
+
require_relative "active_interface/interface_error"
|
8
|
+
require_relative "active_interface/verify"
|
9
|
+
|
10
|
+
module ActiveInterface
|
11
|
+
class Error < StandardError; end
|
12
|
+
# Your code goes here...
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_interface
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Russell Jennings
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-03-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.14'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.14'
|
41
|
+
description: Provide OOP Interfaces for ruby
|
42
|
+
email:
|
43
|
+
- violentpurr@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- Gemfile
|
49
|
+
- Gemfile.lock
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- bin/console
|
54
|
+
- bin/setup
|
55
|
+
- lib/active_interface.rb
|
56
|
+
- lib/active_interface/base.rb
|
57
|
+
- lib/active_interface/contract.rb
|
58
|
+
- lib/active_interface/interface_error.rb
|
59
|
+
- lib/active_interface/verify.rb
|
60
|
+
- lib/active_interface/version.rb
|
61
|
+
homepage: https://github.com/meesterdude/active_interface
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/meesterdude/active_interface
|
66
|
+
source_code_uri: https://github.com/meesterdude/active_interface
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.6.0
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubygems_version: 3.3.3
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: OOP Interfaces for ruby
|
86
|
+
test_files: []
|