conduit 1.0.2 → 1.0.3

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/bin/conduit +5 -0
  3. data/lib/conduit.rb +2 -0
  4. data/lib/conduit/blueprints/actions/base.tt +21 -0
  5. data/lib/conduit/blueprints/base.tt +9 -0
  6. data/lib/conduit/blueprints/bin/generate_action.tt +5 -0
  7. data/lib/conduit/blueprints/decorators.tt +2 -0
  8. data/lib/conduit/blueprints/decorators/base.tt +9 -0
  9. data/lib/conduit/blueprints/driver.tt +14 -0
  10. data/lib/conduit/blueprints/generators/action.tt +61 -0
  11. data/lib/conduit/blueprints/generators/blueprints/action.tt +13 -0
  12. data/lib/conduit/blueprints/generators/blueprints/decorator.tt +6 -0
  13. data/lib/conduit/blueprints/generators/blueprints/mock.tt +7 -0
  14. data/lib/conduit/blueprints/generators/blueprints/parser.tt +8 -0
  15. data/lib/conduit/blueprints/generators/blueprints/rspec_action.tt +4 -0
  16. data/lib/conduit/blueprints/parsers/base.tt +121 -0
  17. data/lib/conduit/blueprints/request_mocker.tt +2 -0
  18. data/lib/conduit/blueprints/request_mocker/base.tt +15 -0
  19. data/lib/conduit/blueprints/spec_helper.tt +8 -0
  20. data/lib/conduit/blueprints/support/helpers.tt +12 -0
  21. data/lib/conduit/blueprints/views/layout.tt +1 -0
  22. data/lib/conduit/cli.rb +146 -0
  23. data/lib/conduit/core/action.rb +90 -26
  24. data/lib/conduit/core/decorator.rb +22 -0
  25. data/lib/conduit/core/request_mocker.rb +89 -0
  26. data/lib/conduit/version.rb +1 -1
  27. data/spec/classes/core/action_spec.rb +67 -0
  28. data/spec/classes/core/request_mocker_spec.rb +45 -0
  29. data/spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb +4 -0
  30. data/spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb +4 -0
  31. data/spec/support/my_driver/request_mockers/foo.rb +13 -0
  32. data/spec/support/xml/failure_mock.xml +4 -0
  33. data/spec/support/xml/success_mock.xml +4 -0
  34. data/spec/support/xml/xml_modified_request.xml +7 -0
  35. data/spec/support/xml/xml_request.xml +2 -2
  36. metadata +69 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae57ae862cb89c3d6032192b7b6222021568d612
4
- data.tar.gz: 25b4c001510e51d24235fd591c86b38284d56840
3
+ metadata.gz: ff4f0e5f7f702375659f970ab9ed86f2ce673ca7
4
+ data.tar.gz: 88ebd51ff6273ba6dad93c01d60f0b78851ddfec
5
5
  SHA512:
6
- metadata.gz: f2ee68b71fbbef5c52dce91b3d9edc8e7add2186d658f8e69b757c00e0ef8c41fd9bfad33e880ae9dfebe9e0539244d9f749efd1d6284751133c714c6a7caa03
7
- data.tar.gz: d33c3ffb5dd8c8bd900cd5ff0decc534939ef9334e0ae2b9160c82bfe007557c13858169b75b17f8245ec455fb471d2a62da9a7168e17fd51998b103f0f62ed0
6
+ metadata.gz: 6106383b60779d51424c96d3319c439a76498c6bc33918f4cdf5363deb6da42666144054210b93bcc0bdb44f4f24e75aa80b9cfd2138f2472d65c977b596a129
7
+ data.tar.gz: 2b6eeb302b22fc3d3e91eb46555d3ad5eeb3c6e698288f2c1624733b02cf39de54a22456a833f5fef969d9ac1e815fcd2d0d5b1d65ce696e9fcafd5669aedc37
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "conduit/cli"
4
+
5
+ Conduit::CLI.start
@@ -23,6 +23,8 @@ module Conduit
23
23
  #
24
24
  autoload :Connection, 'conduit/core/connection'
25
25
  autoload :Render, 'conduit/core/render'
26
+ autoload :Decorator, 'conduit/core/decorator'
27
+ autoload :RequestMocker, 'conduit/core/request_mocker'
26
28
  autoload :Action, 'conduit/core/action'
27
29
  autoload :Parser, 'conduit/core/parser'
28
30
  autoload :Driver, 'conduit/core/driver'
@@ -0,0 +1,21 @@
1
+ #
2
+ # Base action class that all driver actions should extend.
3
+ # Common actions functionality goes here.
4
+ #
5
+
6
+ module Conduit::Driver::<%= classified_name %>
7
+ class Base < Conduit::Core::Action
8
+ class << self
9
+ def inherited(base)
10
+ base.send :required_attributes, *Conduit::Driver::<%= classified_name %>.credentials
11
+ base.send :required_attributes, *Conduit::Driver::<%= classified_name %>.required_attributes
12
+ base.send :optional_attributes, *Conduit::Driver::<%= classified_name %>.optional_attributes
13
+ end
14
+ end
15
+
16
+ def remote_url
17
+ # TODO: Define API endpoint here:
18
+ # http://example.com/api
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ require "conduit"
2
+ require "conduit/<%= underscored_name %>/driver"
3
+ require "conduit/<%= underscored_name %>/version"
4
+ require "conduit/<%= underscored_name %>/request_mocker"
5
+ require "conduit/<%= underscored_name %>/decorators"
6
+
7
+ Conduit.configure do |config|
8
+ config.driver_paths << File.join(File.dirname(__FILE__), "<%= underscored_name %>")
9
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "conduit/<%= underscored_name %>/generators/action"
4
+
5
+ Conduit::<%= classified_name %>::Generators::Action.start
@@ -0,0 +1,2 @@
1
+ mocks = File.join(File.dirname(__FILE__), "decorators", "*.rb")
2
+ Dir.glob(mocks) { |fname| require fname }
@@ -0,0 +1,9 @@
1
+ #
2
+ # Base decorator class that all action decorators should extend.
3
+ # Common decorator functionality goes here.
4
+ #
5
+
6
+ module Conduit::<%= classified_name %>::Decorators
7
+ class Base < Conduit::Core::Decorator
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ require "conduit/<%= underscored_name %>/actions/base"
2
+
3
+ module Conduit
4
+ module Driver
5
+ # Conduit Driver for <%= classified_name %>
6
+ module <%= classified_name %>
7
+ extend Conduit::Core::Driver
8
+
9
+ # TODO: replace `required_credentials` and `optional_attributes` as needed
10
+ required_credentials :username, :password
11
+ optional_attributes :mock_status
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ require "thor/group"
2
+ require "active_support"
3
+ require "byebug"
4
+
5
+ module Conduit
6
+ module <%= classified_name %>
7
+ module Generators
8
+ class Action < Thor::Group
9
+ include Thor::Actions
10
+
11
+ attr_reader :classified_name
12
+ attr_reader :underscored_name
13
+
14
+ argument :name, type: :string
15
+
16
+ def initialize(*args)
17
+ super
18
+ @classified_name = ActiveSupport::Inflector.classify name
19
+ @underscored_name = ActiveSupport::Inflector.underscore name
20
+ @driver_path = "#{File.dirname(__FILE__)}/.."
21
+ @spec_path = "#{@driver_path}/../../../spec"
22
+ end
23
+
24
+ def copy_action
25
+ template("action.tt", "#{@driver_path}/actions/#{@underscored_name}.rb")
26
+ end
27
+
28
+ def copy_decorator
29
+ template("decorator.tt", "#{@driver_path}/decorators/#{@underscored_name}.rb")
30
+ end
31
+
32
+ def copy_parser
33
+ template("parser.tt", "#{@driver_path}/parsers/#{@underscored_name}.rb")
34
+ end
35
+
36
+ def copy_mocks
37
+ template("mock.tt", "#{@driver_path}/request_mocker/#{@underscored_name}.rb")
38
+ create_file "#{@driver_path}/request_mocker/fixtures/responses/#{@underscored_name}/success.xml.erb"
39
+ create_file "#{@driver_path}/request_mocker/fixtures/responses/#{@underscored_name}/failure.xml.erb"
40
+ end
41
+
42
+ def copy_view
43
+ create_file "#{@driver_path}/views/#{@underscored_name}.erb"
44
+ end
45
+
46
+ def add_action_to_driver
47
+ insert_into_file("#{@driver_path}/driver.rb", "\n action :#{@underscored_name}\n", after: "optional_attributes :mock_status\n")
48
+ end
49
+
50
+ def add_spec_files
51
+ template("rspec_action.tt", "#{@spec_path}/conduit/<%= underscored_name %>/actions/#{@underscored_name}_spec.rb")
52
+ create_file "#{@spec_path}/fixtures/requests/#{@underscored_name}/request.xml"
53
+ end
54
+
55
+ def self.source_root
56
+ "#{File.dirname(__FILE__)}/blueprints"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::Driver::<%= classified_name %>
4
+ class <%%= classified_name %> < Conduit::Driver::<%= classified_name %>::Base
5
+ operation :<%%= underscored_name %>
6
+
7
+ # Required attributes go here
8
+ # required_attributes :required_attribute_name
9
+
10
+ # Optional attributes go here
11
+ # optional_attributes :optional_attribute_name
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::<%= classified_name %>::Decorators
4
+ class <%%= classified_name %>Decorator < Base
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::<%= classified_name %>::RequestMocker
4
+ # Request mocker for <%%= underscored_name %>.
5
+ class <%%= classified_name %> < Base
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::Driver::<%= classified_name %>
4
+ class <%%= classified_name %>::Parser < Parser::Base
5
+ # Specify attributes here:
6
+ # attribute :attr_name
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ require "spec_helper"
2
+
3
+ describe <%%= classified_name %> do
4
+ end
@@ -0,0 +1,121 @@
1
+ #
2
+ # Base parser class that all request parsers should extend.
3
+ # Common parser functionality goes here.
4
+ #
5
+ # It uses nokogiri to extract attributes from the xml response.
6
+ #
7
+
8
+ require "nokogiri"
9
+ require "active_support"
10
+
11
+ module Conduit::Driver::<%= classified_name %>
12
+ module Parser
13
+ # Provides the base functionality for parsing
14
+ # XML responses from the api
15
+ class Base < Conduit::Core::Parser
16
+ attr_accessor :xml
17
+
18
+ # Extracts the camelized attribute from the xml response
19
+ def self.attribute(attr_name, &block)
20
+ block ||= lambda do
21
+ content_for "//#{ActiveSupport::Inflector.camelize(attr_name, false)}"
22
+ end
23
+ super(attr_name, &block)
24
+ end
25
+
26
+ def initialize(xml)
27
+ @xml = xml
28
+ end
29
+
30
+ def root
31
+ @root ||= Nokogiri::XML(xml).tap do |doc|
32
+ doc.remove_namespaces!
33
+ end
34
+ end
35
+
36
+ # Get the attribute value at a certain path
37
+ def content_for(path, doc = root)
38
+ node = doc.at_xpath(path)
39
+ node.content if node
40
+ end
41
+
42
+ # Returns the following:
43
+ #
44
+ # - `acknowledged` when async
45
+ # - `success` when successful
46
+ # - `failure` when unsuccessful
47
+ def response_status
48
+ if response_errors.any?
49
+ "failure"
50
+ elsif in_progress?
51
+ "acknowledged"
52
+ else
53
+ "success"
54
+ end
55
+ end
56
+
57
+ # Returns normalized request errors if present.
58
+ def response_errors
59
+ if response_content?
60
+ if !content_for("//Fault").nil?
61
+ [normalized_fault]
62
+ else
63
+ # Returns empty array if no errors
64
+ normalized_errors
65
+ end
66
+ else
67
+ [Conduit::Error.new(message: "Unexpected response from server.")]
68
+ end
69
+ end
70
+
71
+ # Indicates if content present.
72
+ def response_content?
73
+ # TODO: replace this if response not present in `Body`
74
+ !content_for("//Body").nil?
75
+ end
76
+
77
+ private
78
+
79
+ # Indicates if a request is in progress. Used for async requests.
80
+ # Should be overwritten in the action's parser in case true.
81
+ def in_progress?
82
+ false
83
+ end
84
+
85
+ # Returns a new `Conduit::Error` instance containing the fault text
86
+ # and the fault code.
87
+ def normalized_fault
88
+ # TODO: replace this if error message and code not present in the
89
+ # below paths
90
+ Conduit::Error.new({
91
+ message: content_for("//Fault//Text"),
92
+ code: content_for("//Fault//Code//Value")
93
+ })
94
+ end
95
+
96
+ # Extracts the error from either `errorMessage` or `errorText`
97
+ def error_response
98
+ # TODO: replace this if error response not present in any of the following
99
+ # paths.
100
+ content_for("//errorMessage") || content_for("//errorText")
101
+ end
102
+
103
+ # Returns a new `Conduit::Error` instance containing the error message and
104
+ # its code.
105
+ def normalized_errors
106
+ # TODO: replace this if error not present in `error_message` and code
107
+ # not present in `errorNumber`
108
+ if error_response && !error_response.empty?
109
+ [
110
+ Conduit::Error.new({
111
+ message: error_response,
112
+ code: content_for("errorNumber")
113
+ })
114
+ ]
115
+ else
116
+ []
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,2 @@
1
+ mocks = File.join(File.dirname(__FILE__), "request_mocker", "*.rb")
2
+ Dir.glob(mocks) { |fname| require fname }
@@ -0,0 +1,15 @@
1
+ #
2
+ # Base request mocker class that all mockers should extend.
3
+ # Common mock functionality goes here.
4
+ #
5
+
6
+ module Conduit::<%= classified_name %>::RequestMocker
7
+ class Base < Conduit::Core::RequestMocker
8
+ private
9
+
10
+ # Default path for fixtures.
11
+ def fixtures_path
12
+ "#{__dir__}/fixtures/responses"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ require "rspec/its"
2
+ require "conduit/<%= underscored_name %>"
3
+ require "rspec/matchers"
4
+ require "equivalent-xml"
5
+
6
+ include Conduit::Driver::<%= classified_name %>
7
+
8
+ Dir[File.join(Dir.pwd, "spec/support/**/*.rb")].each { |file| require file }
@@ -0,0 +1,12 @@
1
+ module Helpers
2
+ def credentials
3
+ {
4
+ mock_status: :success
5
+ }
6
+ end
7
+ end
8
+
9
+ RSpec.configure do |config|
10
+ # rspec configuration goes here
11
+ config.include Helpers
12
+ end
@@ -0,0 +1 @@
1
+ <%%= yield %>
@@ -0,0 +1,146 @@
1
+ require "thor"
2
+ require "active_support"
3
+ require "bundler"
4
+
5
+ module Conduit
6
+ class CLI < Thor
7
+ include Thor::Actions
8
+
9
+ attr_accessor :driver_name, :classified_name, :underscored_name, :base_path
10
+
11
+ desc "generate_driver DRIVER_NAME", "Generates a new xml `conduit-driver` gem"
12
+ long_desc <<-LONGDESC
13
+ `generate_driver DRIVER_NAME` will generate a new gem for the xml conduit
14
+ driver.
15
+
16
+ It will add the base `action`, `decorator`, `parser` and `request_mocker`
17
+ files along with generators and other basic files.
18
+ LONGDESC
19
+ def generate_driver driver_name
20
+ @driver_name = driver_name
21
+ @dasherized_name = ActiveSupport::Inflector.dasherize driver_name
22
+ @classified_name = ActiveSupport::Inflector.classify driver_name
23
+ @underscored_name = ActiveSupport::Inflector.underscore driver_name
24
+ @pwd = Dir.pwd
25
+ @base_path = "#{@pwd}/conduit-#{@dasherized_name}"
26
+
27
+ run("bundle gem conduit-#{@underscored_name} --no-coc --mit --test=rspec")
28
+ fix_names
29
+ copy_files
30
+ modify_files
31
+ run("chmod +x #{@base_path}/bin/generate_action")
32
+ bundle_install
33
+ end
34
+
35
+ private
36
+
37
+ # In case the name of the driver is composed of multiple words we need to
38
+ # generate the gem with an underscored named (in order to avoid nested modules
39
+ # for each part of the name) and then replace the gem and base file name with
40
+ # the dasherized one.
41
+ # For example, if the driver is called `best-wireless` we create `conduit-best_driver`
42
+ # gem and we change the name to `conduit-best-driver`
43
+ def fix_names
44
+ if @underscored_name.split("_").length > 1
45
+ current_path = "#{@pwd}/conduit-#{@underscored_name}"
46
+
47
+ # Change gem name in gemspec file
48
+ gemspec_file = "#{current_path}/conduit-#{@underscored_name}.gemspec"
49
+ gsub_file(gemspec_file, /spec\.name(.*)/, "spec.name = \"conduit-#{@dasherized_name}\"")
50
+
51
+ # Rename gemspec file
52
+ run("mv #{gemspec_file} #{current_path}/conduit-#{@dasherized_name}.gemspec")
53
+
54
+ # Rename gem dir
55
+ run("mv #{current_path} #{@base_path}")
56
+ end
57
+ end
58
+
59
+ def bundle_install
60
+ # Taken from https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/railties/lib/rails/generators/app_base.rb#L363
61
+ _bundle_command = Gem.bin_path("bundler", "bundle")
62
+
63
+ Bundler.with_clean_env do
64
+ inside(@base_path) do
65
+ full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" install]
66
+ run(full_command)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Indicates where the template files are located
72
+ def self.source_root
73
+ "#{__dir__}/blueprints"
74
+ end
75
+
76
+ # List of the template files and their destination
77
+ def files_to_copy
78
+ lib_path = "#{@base_path}/lib/conduit/#{@underscored_name}"
79
+
80
+ {
81
+ "base.tt" => "#{@base_path}/lib/conduit/#{@underscored_name}.rb",
82
+ "actions/base.tt" => "#{lib_path}/actions/base.rb",
83
+ "decorators/base.tt" => "#{lib_path}/decorators/base.rb",
84
+ "parsers/base.tt" => "#{lib_path}/parsers/base.rb",
85
+ "request_mocker/base.tt" => "#{lib_path}/request_mocker/base.rb",
86
+ "views/layout.tt" => "#{lib_path}/views/layout.erb",
87
+ "decorators.tt" => "#{lib_path}/decorators.rb",
88
+ "driver.tt" => "#{lib_path}/driver.rb",
89
+ "request_mocker.tt" => "#{lib_path}/request_mocker.rb",
90
+ "generators/action.tt" => "#{lib_path}/generators/action.rb",
91
+ "generators/blueprints/action.tt" => "#{lib_path}/generators/blueprints/action.tt",
92
+ "generators/blueprints/decorator.tt" => "#{lib_path}/generators/blueprints/decorator.tt",
93
+ "generators/blueprints/mock.tt" => "#{lib_path}/generators/blueprints/mock.tt",
94
+ "generators/blueprints/parser.tt" => "#{lib_path}/generators/blueprints/parser.tt",
95
+ "generators/blueprints/rspec_action.tt" => "#{lib_path}/generators/blueprints/rspec_action.tt",
96
+ "bin/generate_action.tt" => "#{@base_path}/bin/generate_action",
97
+ "support/helpers.tt" => "#{@base_path}/spec/support/helpers.rb",
98
+ "spec_helper.tt" => "#{@base_path}/spec/spec_helper.rb"
99
+ }
100
+ end
101
+
102
+ # Copy template files
103
+ def copy_files
104
+ files_to_copy.each do |origin, destination|
105
+ template(origin, destination, force: true)
106
+ end
107
+ end
108
+
109
+ # Adds missing lines to the files
110
+ def modify_files
111
+ gemspec_file = "#{@base_path}/conduit-#{@dasherized_name}.gemspec"
112
+
113
+ # add gemspec dependencies
114
+ str = " # Dependencies\n"\
115
+ " #\n"\
116
+ " spec.add_dependency \"conduit\", \"~> 1.0.3\"\n"\
117
+ " # xml parser\n"\
118
+ " spec.add_dependency \"nokogiri\"\n"\
119
+ " # to get string inflectors\n"\
120
+ " spec.add_dependency \"activesupport\"\n\n"\
121
+ " # Development Dependencies\n"\
122
+ " #\n"\
123
+ " spec.add_development_dependency \"shoulda-matchers\"\n"\
124
+ " # to compare xml files in tests\n"\
125
+ " spec.add_development_dependency \"equivalent-xml\"\n"\
126
+ " spec.add_development_dependency \"rspec-its\"\n"\
127
+ " # for building CLI\n"\
128
+ " spec.add_development_dependency \"thor\"\n"\
129
+ " # for debugging\n"\
130
+ " spec.add_development_dependency \"byebug\"\n"
131
+
132
+ insert_into_file gemspec_file, str, after: "spec.require_paths = [\"lib\"]\n\n"
133
+
134
+ # remove description
135
+ gsub_file(gemspec_file, /spec\.description(.*)\n/, "")
136
+
137
+ # update summary
138
+ new_summary = "spec.summary = \"#{ActiveSupport::Inflector.humanize @underscored_name} Driver for Conduit\""
139
+ gsub_file(gemspec_file, /spec\.summary(.*)/, new_summary)
140
+
141
+ # update homepage
142
+ new_homepage = "spec.homepage = \"http://www.github.com/conduit/conduit-#{@dasherized_name}\""
143
+ gsub_file(gemspec_file, /spec\.homepage(.*)/, new_homepage)
144
+ end
145
+ end
146
+ end
@@ -27,6 +27,7 @@ module Conduit
27
27
  base.send :include, Conduit::Core::Connection
28
28
  base.send :include, Conduit::Core::Render
29
29
  base.send :include, InstanceMethods
30
+
30
31
  base.extend ClassMethods
31
32
 
32
33
  # TODO: Move this to the driver scope
@@ -87,22 +88,30 @@ module Conduit
87
88
  def_delegator :'self.class', :requirements
88
89
  def_delegator :'self.class', :attributes
89
90
 
90
- # def requirements
91
- # self.class.requirements
92
- # end
93
-
94
- # def attributes
95
- # self.class.attributes
96
- # end
97
-
98
- # Object used for passing data to the view
99
- # Only keys listed in attributes will be
100
- # used.
91
+ # The view's context will be the action's decorator.
92
+ # The decorator name depends on the current action's name.
93
+ #
94
+ # For example `Conduit::MyDriver::Decorators::ActivateDecorator`
95
+ # will be the view's context for the `activate` action.
101
96
  #
102
97
  def view_context
103
- OpenStruct.new(@options.select do |k, v|
104
- attributes.include?(k)
105
- end)
98
+ view_decorator.new(
99
+ OpenStruct.new(attributes_with_values)
100
+ )
101
+ end
102
+
103
+ # Returns a hash of all the defined attributes and
104
+ # their values.
105
+ #
106
+ # If an attribute's value is not passed as an option it
107
+ # will default to `nil`.
108
+ #
109
+ def attributes_with_values
110
+ attributes.inject({}) do |hash, attribute|
111
+ hash.tap do |h|
112
+ h[attribute] = @options[attribute]
113
+ end
114
+ end
106
115
  end
107
116
 
108
117
  # Location where the view files can be found
@@ -121,24 +130,41 @@ module Conduit
121
130
  render(tpl)
122
131
  end
123
132
 
124
- # Entry method. The class will use
125
- # use this to trigger the request.
133
+ # Method called to make the actual request.
126
134
  #
127
135
  # Override to customize.
128
136
  #
129
- def perform
137
+ def perform_request
130
138
  response = request(body: view, method: :post)
131
- parser = parser_class.new(response.body)
139
+ parser_instance = parser.new(response.body)
140
+
141
+ Conduit::ApiResponse.new(raw_response: response, parser: parser_instance)
142
+ end
132
143
 
133
- Conduit::ApiResponse.new(raw_response: response,
134
- parser: parser)
144
+ # Entry method. Calls either the mocker or the `perform_request`
145
+ # method.
146
+ #
147
+ def perform
148
+ # When testing we can mock the request. The mocker used
149
+ # will depend on the action's name. For example:
150
+ # `Conduit::MyDriver::RequestMocker::Activate` will be responsible for
151
+ # mocking the `activate` action.
152
+ #
153
+ # * To mock success pass `mock_status: 'success'` as an option.
154
+ # * To mock failure pass `mock_status: 'failure'` as an option.
155
+ if mock_mode?
156
+ mocker = request_mocker.new(self, @options)
157
+ mocker.with_mocking { perform_request }
158
+ else
159
+ perform_request
160
+ end
135
161
  end
136
162
 
137
163
  private
138
164
 
139
165
  # Ensures that all required attributes are present
140
166
  # If not all attributes are present, will raise
141
- # an ArgumentError listing missing attributes
167
+ # an ArgumentError listing missing attributes.
142
168
  #
143
169
  def validate!(options)
144
170
  missing_keys = (requirements.to_a - options.keys)
@@ -148,15 +174,53 @@ module Conduit
148
174
  end
149
175
  end
150
176
 
151
- # Returns the parser for this action
152
- # subclasses responsible for providing the
153
- # response_body and response_status.
177
+ # Returns the capitalized action name.
178
+ #
179
+ def action_name
180
+ ActiveSupport::Inflector.demodulize(self.class)
181
+ end
182
+
183
+ # Returns the name of the driver that this action belongs to.
184
+ #
185
+ def driver_name
186
+ self.class.to_s.deconstantize.demodulize.underscore
187
+ end
188
+
189
+ # Returns the parser for this action subclasses responsible for providing
190
+ # the response_body and response_status.
154
191
  #
155
- def parser_class
192
+ def parser
156
193
  self.class.const_get(:Parser)
157
194
  end
158
- end
159
195
 
196
+ # Returns the mocker class to use based on the action's name.
197
+ #
198
+ def request_mocker
199
+ "Conduit::#{driver_name.classify}::RequestMocker::#{action_name}".constantize
200
+ end
201
+
202
+ # Returns the decorator class to use based on the action's name. If no
203
+ # decorator class defined, it will return the core decorator class.
204
+ #
205
+ def view_decorator
206
+ # In ruby 2.0.0 we can't do `const_defined? MyDriver::Decorators::FooDecorator`
207
+ # so we have to check each module one by one.
208
+ if Conduit.const_defined?(driver_name.classify) &&
209
+ Conduit.const_get(driver_name.classify).const_defined?("Decorators") &&
210
+ Conduit.const_get("#{driver_name.classify}::Decorators").const_defined?("#{action_name}Decorator")
211
+
212
+ Conduit.const_get("#{driver_name.classify}::Decorators::#{action_name}Decorator")
213
+ else
214
+ Conduit::Core::Decorator
215
+ end
216
+ end
217
+
218
+ # Indicates whether the request should be mocked or not.
219
+ #
220
+ def mock_mode?
221
+ @options.has_key?(:mock_status) && (!@options[:mock_status].nil? && !@options[:mock_status].empty?)
222
+ end
223
+ end
160
224
  end
161
225
  end
162
226
  end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Decorators are used to act as the view's
3
+ # context. This is where most of the logic occurs,
4
+ # mainly where options are organized in a hash
5
+ # for the view to serialize.
6
+ #
7
+ # Extend this class for every action. For example,
8
+ # for the `activate` action, an `ActivateDecorator`
9
+ # is defined which exposes a `line_activation_attributes` method
10
+ # for the view.
11
+ #
12
+
13
+ require 'delegate'
14
+ require 'forwardable'
15
+
16
+ module Conduit
17
+ module Core
18
+ class Decorator < SimpleDelegator
19
+ extend Forwardable
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ #
2
+ # Mocks action responses for testing. We can choose
3
+ # the type of response (success, failure, error).
4
+ #
5
+ # Response fixtures are stored in a directory specified inside
6
+ # `fixtures_path`. Each action has its own folder and each response file
7
+ # is named according to the status we're mocking.
8
+ #
9
+ # In order to mock a request, pass the option `mock_status` to
10
+ # the action with its value being the returned status.
11
+ #
12
+
13
+ require 'erb'
14
+ require 'excon'
15
+ require 'tilt'
16
+
17
+ module Conduit
18
+ module Core
19
+ class RequestMocker
20
+ def initialize(base, options = nil)
21
+ @base = base
22
+ @options = options
23
+ @mock_status = options[:mock_status].to_sym || :success
24
+ @http_status = options[:http_status] || 200
25
+ end
26
+
27
+ # Starts mocking, override if using something different than
28
+ # excon to make the requests.
29
+ #
30
+ def mock
31
+ Excon.defaults[:mock] = true
32
+ Excon.stub({}, { body: response, status: @http_status })
33
+ end
34
+
35
+ # Ends mocking, override if using something different than
36
+ # excon to make the requests.
37
+ #
38
+ def unmock
39
+ Excon.defaults[:mock] = false
40
+ Excon.stubs.clear
41
+ end
42
+
43
+ # Wrapps the block inside a mocking state.
44
+ #
45
+ def with_mocking
46
+ mock and yield.tap { unmock }
47
+ end
48
+
49
+ private
50
+
51
+ def render_response
52
+ Tilt::ERBTemplate.new(fixture).render(@base.view_context)
53
+ end
54
+
55
+ def action_name
56
+ ActiveSupport::Inflector.demodulize(self.class.name).underscore
57
+ end
58
+
59
+ # Directory where fixtures are stored.
60
+ #
61
+ def fixtures_path
62
+ raise NoMethodError, 'Please define fixtures_path in your request mocker.'
63
+ end
64
+
65
+ def fixture
66
+ "#{fixtures_path}/#{action_name}/#{@mock_status}.xml.erb"
67
+ end
68
+
69
+ def response
70
+ if response_statuses.include?(@mock_status)
71
+ return error_response if @mock_status == :error
72
+ render_response
73
+ else
74
+ raise(ArgumentError, "Mock status must be one of the following: #{response_statuses.join(', ')}")
75
+ end
76
+ end
77
+
78
+ # Allowed statuses.
79
+ def response_statuses
80
+ %i(success failure error)
81
+ end
82
+
83
+ # Mocked error response.
84
+ def error_response
85
+ '{"status": 500, "error": "Internal Service Error"}'
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,3 @@
1
1
  module Conduit
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.3'
3
3
  end
@@ -3,6 +3,7 @@ require 'spec_helper'
3
3
  shared_examples_for Conduit::Core::Action do
4
4
 
5
5
  let(:request) { read_support_file('xml/xml_request.xml') }
6
+ let(:modified_request) { read_support_file('xml/xml_modified_request.xml') }
6
7
  let(:response) { read_support_file('xml/xml_response.xml') }
7
8
 
8
9
  subject do
@@ -28,6 +29,19 @@ shared_examples_for Conduit::Core::Action do
28
29
  end
29
30
 
30
31
  context 'with an instance' do
32
+ describe '#attributes_with_values' do
33
+ context 'with optional attribute not set' do
34
+ let (:attrs) { request_attributes.merge(buz: nil) }
35
+ its(:attributes_with_values) { should eq(attrs) }
36
+ end
37
+
38
+ context 'with optional attribute set' do
39
+ let (:attrs) { request_attributes.merge(buz: "value for buz") }
40
+ subject { described_class.new(attrs) }
41
+ its(:attributes_with_values) { should eq(attrs) }
42
+ end
43
+ end
44
+
31
45
  describe '#view' do
32
46
  before do
33
47
  Excon.stub({}, body: request, status: 200)
@@ -40,6 +54,59 @@ shared_examples_for Conduit::Core::Action do
40
54
  end
41
55
  end
42
56
 
57
+ describe '#view_context' do
58
+ context 'without decorator class' do
59
+ it 'returns an instance of core decorator' do
60
+ subject.view_context.should be_a_kind_of(Conduit::Core::Decorator)
61
+ end
62
+ end
63
+
64
+ context 'with decorator class' do
65
+ let(:driver) { subject.class.name.deconstantize.demodulize }
66
+ let(:action) { subject.class.name.demodulize }
67
+
68
+ # Define decorator module programmatically:
69
+ let(:decorator) do
70
+ # Create DriverName::Decorators module
71
+ module TheDriver
72
+ module Decorators
73
+ end
74
+ end
75
+
76
+ # Add action decorator to DriverName::Decorators
77
+ class TheDecorator < Conduit::Core::Decorator
78
+ def foo
79
+ "new #{super}"
80
+ end
81
+
82
+ def baz
83
+ "modified baz"
84
+ end
85
+ end
86
+
87
+ # Set the above defined modules and class in Conduit
88
+ stub_const("TheDriver::Decorators::#{action}Decorator", TheDecorator)
89
+
90
+ TheDriver
91
+ end
92
+
93
+ before do
94
+ stub_const("Conduit::#{driver}", decorator)
95
+ Excon.stub({}, body: modified_request, status: 200)
96
+ end
97
+
98
+ it 'returns an instance of core decorator' do
99
+ subject.view_context.should be_a_kind_of(Conduit.const_get(driver)::Decorators::const_get("#{action}Decorator"))
100
+ end
101
+
102
+ it 'returns a new rendered view for an action' do
103
+ a = subject.view.gsub(/\s+/, '')
104
+ b = modified_request.gsub(/\s+/, '')
105
+ a.should == b
106
+ end
107
+ end
108
+ end
109
+
43
110
  describe '#perform' do
44
111
  before { Excon.stub({}, body: response, status: 200) }
45
112
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'excon'
3
+
4
+ shared_examples_for Conduit::Core::RequestMocker do
5
+
6
+ let(:success) { read_support_file('xml/success_mock.xml') }
7
+ let(:failure) { read_support_file('xml/failure_mock.xml') }
8
+ let(:fake_action) do
9
+ class FakeAction
10
+ def view_context
11
+ OpenStruct.new({ value: "Yay!" })
12
+ end
13
+ end
14
+
15
+ FakeAction.new
16
+ end
17
+
18
+ context 'success mocking' do
19
+ subject { described_class.new(fake_action, { mock_status: 'success' }) }
20
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
21
+ it 'return success response' do
22
+ response.should eq(success)
23
+ end
24
+ end
25
+
26
+ context 'failure mocking' do
27
+ subject { described_class.new(fake_action, { mock_status: 'failure' }) }
28
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
29
+ it 'return failure response' do
30
+ response.should eq(failure)
31
+ end
32
+ end
33
+
34
+ context 'with disallowed response status' do
35
+ subject { described_class.new(fake_action, { mock_status: 'fake_status' }) }
36
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
37
+ it 'raises and error' do
38
+ lambda { response }.should raise_error(ArgumentError)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe Conduit::MyDriver::RequestMocker::Foo do
44
+ it_behaves_like Conduit::Core::RequestMocker
45
+ end
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="failure" action="foo" />
3
+ <error>Err 404</error>
4
+ </BeQuick>
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="success" action="foo" />
3
+ <val><%= value %></val>
4
+ </BeQuick>
@@ -0,0 +1,13 @@
1
+ module Conduit
2
+ module MyDriver
3
+ module RequestMocker
4
+ class Foo < Conduit::Core::RequestMocker
5
+ private
6
+
7
+ def fixtures_path
8
+ "#{__dir__}/fixtures"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="failure" action="foo" />
3
+ <error>Err 404</error>
4
+ </BeQuick>
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="success" action="foo" />
3
+ <val>Yay!</val>
4
+ </BeQuick>
@@ -0,0 +1,7 @@
1
+ <BeQuick product="conduit">
2
+ <action name="foo">
3
+ <foo>new value for foo</foo>
4
+ <bar>value for bar</bar>
5
+ <baz>modified baz</baz>
6
+ </action>
7
+ </BeQuick>
@@ -1,7 +1,7 @@
1
- <BeQuickproduct="conduit">
1
+ <BeQuick product="conduit">
2
2
  <action name="foo">
3
3
  <foo>value for foo</foo>
4
4
  <bar>value for bar</bar>
5
5
  <baz>value for baz</baz>
6
6
  </action>
7
- </BeQuick>
7
+ </BeQuick>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conduit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Kelley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-24 00:00:00.000000000 Z
11
+ date: 2016-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: shoulda-matchers
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -125,20 +153,43 @@ dependencies:
125
153
  description:
126
154
  email:
127
155
  - mike@codezombie.org
128
- executables: []
156
+ executables:
157
+ - conduit
129
158
  extensions: []
130
159
  extra_rdoc_files: []
131
160
  files:
132
161
  - Rakefile
162
+ - bin/conduit
133
163
  - lib/conduit.rb
134
164
  - lib/conduit/api_response.rb
165
+ - lib/conduit/blueprints/actions/base.tt
166
+ - lib/conduit/blueprints/base.tt
167
+ - lib/conduit/blueprints/bin/generate_action.tt
168
+ - lib/conduit/blueprints/decorators.tt
169
+ - lib/conduit/blueprints/decorators/base.tt
170
+ - lib/conduit/blueprints/driver.tt
171
+ - lib/conduit/blueprints/generators/action.tt
172
+ - lib/conduit/blueprints/generators/blueprints/action.tt
173
+ - lib/conduit/blueprints/generators/blueprints/decorator.tt
174
+ - lib/conduit/blueprints/generators/blueprints/mock.tt
175
+ - lib/conduit/blueprints/generators/blueprints/parser.tt
176
+ - lib/conduit/blueprints/generators/blueprints/rspec_action.tt
177
+ - lib/conduit/blueprints/parsers/base.tt
178
+ - lib/conduit/blueprints/request_mocker.tt
179
+ - lib/conduit/blueprints/request_mocker/base.tt
180
+ - lib/conduit/blueprints/spec_helper.tt
181
+ - lib/conduit/blueprints/support/helpers.tt
182
+ - lib/conduit/blueprints/views/layout.tt
183
+ - lib/conduit/cli.rb
135
184
  - lib/conduit/configuration.rb
136
185
  - lib/conduit/connection_error.rb
137
186
  - lib/conduit/core/action.rb
138
187
  - lib/conduit/core/connection.rb
188
+ - lib/conduit/core/decorator.rb
139
189
  - lib/conduit/core/driver.rb
140
190
  - lib/conduit/core/parser.rb
141
191
  - lib/conduit/core/render.rb
192
+ - lib/conduit/core/request_mocker.rb
142
193
  - lib/conduit/drivers/keep
143
194
  - lib/conduit/error.rb
144
195
  - lib/conduit/storage.rb
@@ -150,14 +201,21 @@ files:
150
201
  - spec/classes/core/action_spec.rb
151
202
  - spec/classes/core/driver_spec.rb
152
203
  - spec/classes/core/parser_spec.rb
204
+ - spec/classes/core/request_mocker_spec.rb
153
205
  - spec/classes/util_spec.rb
154
206
  - spec/spec_helper.rb
155
207
  - spec/support/helper.rb
156
208
  - spec/support/my_driver/actions/foo.rb
157
209
  - spec/support/my_driver/driver.rb
158
210
  - spec/support/my_driver/parsers/foo.rb
211
+ - spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb
212
+ - spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb
213
+ - spec/support/my_driver/request_mockers/foo.rb
159
214
  - spec/support/my_driver/views/foo.erb
160
215
  - spec/support/my_driver/views/layout.erb
216
+ - spec/support/xml/failure_mock.xml
217
+ - spec/support/xml/success_mock.xml
218
+ - spec/support/xml/xml_modified_request.xml
161
219
  - spec/support/xml/xml_request.xml
162
220
  - spec/support/xml/xml_response.xml
163
221
  homepage: https://github.com/conduit/conduit
@@ -179,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
237
  version: '0'
180
238
  requirements: []
181
239
  rubyforge_project:
182
- rubygems_version: 2.6.3
240
+ rubygems_version: 2.4.5
183
241
  signing_key:
184
242
  specification_version: 4
185
243
  summary: Conduit is an interface for debit platforms.
@@ -187,14 +245,20 @@ test_files:
187
245
  - spec/classes/core/action_spec.rb
188
246
  - spec/classes/core/driver_spec.rb
189
247
  - spec/classes/core/parser_spec.rb
248
+ - spec/classes/core/request_mocker_spec.rb
190
249
  - spec/classes/util_spec.rb
191
250
  - spec/spec_helper.rb
192
251
  - spec/support/helper.rb
193
252
  - spec/support/my_driver/actions/foo.rb
194
253
  - spec/support/my_driver/driver.rb
195
254
  - spec/support/my_driver/parsers/foo.rb
255
+ - spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb
256
+ - spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb
257
+ - spec/support/my_driver/request_mockers/foo.rb
196
258
  - spec/support/my_driver/views/foo.erb
197
259
  - spec/support/my_driver/views/layout.erb
260
+ - spec/support/xml/failure_mock.xml
261
+ - spec/support/xml/success_mock.xml
262
+ - spec/support/xml/xml_modified_request.xml
198
263
  - spec/support/xml/xml_request.xml
199
264
  - spec/support/xml/xml_response.xml
200
- has_rdoc: