interfacer 0.0.1

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.
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: []