interfacer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/README.md +104 -0
  4. data/lib/interfacer.rb +59 -0
  5. metadata +61 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a833ad6c374e5255b3390d46b0f8bac7f14d040f88a255182efc159170d1b6f
4
+ data.tar.gz: 3d40f9c176ce3ef82c164780d09c6efdf77212ca8cb1d461e06166011af43fc2
5
+ SHA512:
6
+ metadata.gz: 9c20b5f30a63ced573767ec4a4cb9d7b2b772aace4757a610564ea041465ffb72b4658fb50f9d53fa970cd95f88a97f78a3a3139f76e391ba8b931035d912240
7
+ data.tar.gz: b2564c14983ca1732bc40f25487243b747470b4781106e3dd654e4f7b8a3fc3b0784f756fdffb6980b9746bc47180e9b52510002bb492266007a3e7b00705c54
@@ -0,0 +1,7 @@
1
+ --output-dir=yardoc
2
+ --markup-provider=redcarpet
3
+ --markup=markdown
4
+ --no-private
5
+ --embed-mixins
6
+ --plugin rspec
7
+ --files doc/*md
@@ -0,0 +1,104 @@
1
+ # About
2
+
3
+ [![Gem version][GV img]][Gem version]
4
+ [![Build status][BS img]][Build status]
5
+ [![Coverage status][CS img]][Coverage status]
6
+ [![CodeClimate status][CC img]][CodeClimate status]
7
+ [![YARD documentation][YD img]][YARD documentation]
8
+
9
+ How much abstraction is too much?
10
+
11
+ On one hand <abbr title="Inversion of control">IoC</abbr> is probably a too heavy canon for a dynamic language like Ruby, instantiating external dependencies in `#initalize` doesn't sound like a good idea either.
12
+
13
+ If I have to replace a dependency, I want to know what interface it has to provide.
14
+
15
+ That is what methods is my project actually using, so not the same as `class MyCollection implements ListInterface`.
16
+
17
+ # Example
18
+
19
+ ```ruby
20
+ require 'import'
21
+
22
+ Interfacer = import('interfacer').Interfacer
23
+
24
+ class Post
25
+ extend Interfacer
26
+
27
+ attribute(:time_class, '.now', '#to_s') { Time }
28
+
29
+ def publish!
30
+ "~ Post #{self.inspect} has been published at #{self.time_class.now} (using #{self.time_class})."
31
+ end
32
+ end
33
+
34
+ puts "~ With the default time_class."
35
+ post = Post.new
36
+ puts post.publish!
37
+
38
+ puts "\n~ With overriden time_class."
39
+ require 'date'
40
+
41
+ post = Post.new
42
+ post.time_class = DateTime
43
+ puts post.publish!
44
+ ```
45
+
46
+ To me, this is what I consider to be the golden middle way. There's nearly no extra code, no factory method etc, but I can replace the time class any time I want.
47
+
48
+ Why?
49
+
50
+ ```ruby
51
+ class TimeMock
52
+ class << self
53
+ alias_method :now, :new
54
+ end
55
+
56
+ def to_s
57
+ 'Monday evening'
58
+ end
59
+ end
60
+
61
+ describe Post do
62
+ before(:each) do
63
+ subject.time_class = TimeMock
64
+ end
65
+
66
+ it "prints out when a post was published" do
67
+ expect(post.publish!).to match(/has been published at Monday evening/)
68
+ end
69
+ end
70
+ ```
71
+
72
+ You can say that you could just stub `Time.now` and you're right, but I'm not a huge fan of that approach. I like clear dependencies, actual objects and (on a slightly different subject, but still vaguely related) I think tests should test public APIs and not order in which things are executed (when used mocks), because everything breaks when you do internal refactoring.
73
+
74
+ But whatever, let's have an another example.
75
+
76
+ ```ruby
77
+ settings = import('settings')
78
+
79
+ class Post
80
+ attribute :json_encoder, '.generate' { settings.json_encoder }
81
+ end
82
+ ```
83
+
84
+ Now when you decide to switch to say `oj`, all you have to do is this:
85
+
86
+ ```ruby
87
+ # settings.rb
88
+
89
+ require 'oj'
90
+
91
+ export json_encoder: Oj
92
+ ```
93
+
94
+ [Gem version]: https://rubygems.org/gems/interfacer
95
+ [Build status]: https://travis-ci.org/botanicus/interfacer
96
+ [Coverage status]: https://coveralls.io/github/botanicus/interfacer
97
+ [CodeClimate status]: https://codeclimate.com/github/botanicus/interfacer/maintainability
98
+ [YARD documentation]: http://www.rubydoc.info/github/botanicus/interfacer/master
99
+
100
+ [GV img]: https://badge.fury.io/rb/interfacer.svg
101
+ [BS img]: https://travis-ci.org/botanicus/interfacer.svg?branch=master
102
+ [CS img]: https://img.shields.io/coveralls/botanicus/interfacer.svg
103
+ [CC img]: https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability
104
+ [YD img]: http://img.shields.io/badge/yard-docs-blue.svg
@@ -0,0 +1,59 @@
1
+ # @api private
2
+ class InterfaceSpec
3
+ def initialize(required_interface_methods)
4
+ @required_interface_methods = required_interface_methods
5
+ end
6
+
7
+ def missing_methods(tested_class)
8
+ @required_interface_methods.reject do |method_name|
9
+ if method_name[0] == '.'
10
+ tested_class.respond_to?(method_name[1..-1])
11
+ elsif method_name[0] == '#'
12
+ tested_class.instance_methods.include?(method_name[1..-1].to_sym)
13
+ else
14
+ raise ArgumentError.new("Incorrect method name. Method name must start with either . or #, such as .new or #to_s.")
15
+ end
16
+ end
17
+ end
18
+
19
+ def fullfills_interface?(tested_class)
20
+ self.missing_methods(tested_class).empty?
21
+ end
22
+ end
23
+
24
+ # @api public
25
+ class InterfaceRequirementsNotMetError < StandardError
26
+ def initialize(attr_name, missing_methods)
27
+ super("Attribute #{attr_name} expects #{missing_methods.inspect} to be defined.")
28
+ end
29
+ end
30
+
31
+ # @api public
32
+ module Interfacer
33
+ def interface_specs_for_attrs
34
+ @interface_specs_for_attrs ||= Hash.new
35
+ end
36
+
37
+ def attribute(attr_name, *required_methods, &block)
38
+ interface_specs_for_attrs[attr_name] = InterfaceSpec.new(required_methods)
39
+
40
+ define_method(attr_name) do
41
+ value = instance_variable_get(:"@#{attr_name}")
42
+ return value if value
43
+ instance_variable_set(:"@#{attr_name}", block.call)
44
+ end
45
+
46
+ define_method(:"#{attr_name}=") do |value|
47
+ spec = self.class.interface_specs_for_attrs[attr_name]
48
+
49
+ unless spec.fullfills_interface?(value)
50
+ raise InterfaceRequirementsNotMetError.new(attr_name, spec.missing_methods(value))
51
+ end
52
+
53
+ instance_variable_set(:"@#{attr_name}", value)
54
+ end
55
+ end
56
+ end
57
+
58
+ export Interfacer: Interfacer,
59
+ InterfaceRequirementsNotMetError: InterfaceRequirementsNotMetError
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interfacer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - James C Russell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: commonjs_modules
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.0'
27
+ description: "."
28
+ email: james@101ideas.cz
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".yardopts"
34
+ - README.md
35
+ - lib/interfacer.rb
36
+ homepage: http://github.com/botanicus/interfacer
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ yard.run: yri
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 2.7.6
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: ''
61
+ test_files: []