kono_epp_client 0.1.0 → 2.0.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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/Makefile +13 -0
  4. data/README.md +33 -0
  5. data/Rakefile +6 -0
  6. data/kono_epp_client.gemspec +42 -0
  7. data/lib/kono_epp_client/VERSION +1 -0
  8. data/lib/kono_epp_client/commands/check_contacts.rb +19 -0
  9. data/lib/{epp/epp_command/kono_epp_check_domains.rb → kono_epp_client/commands/check_domains.rb} +3 -1
  10. data/lib/kono_epp_client/commands/command.rb +15 -0
  11. data/lib/kono_epp_client/commands/create_contact.rb +89 -0
  12. data/lib/kono_epp_client/commands/create_domain.rb +56 -0
  13. data/lib/kono_epp_client/commands/delete_contact.rb +17 -0
  14. data/lib/kono_epp_client/commands/delete_domain.rb +16 -0
  15. data/lib/kono_epp_client/commands/hello.rb +15 -0
  16. data/lib/kono_epp_client/commands/info_contact.rb +17 -0
  17. data/lib/kono_epp_client/commands/info_domain.rb +16 -0
  18. data/lib/kono_epp_client/commands/login.rb +84 -0
  19. data/lib/kono_epp_client/commands/logout.rb +12 -0
  20. data/lib/kono_epp_client/commands/poll.rb +18 -0
  21. data/lib/kono_epp_client/commands/transfer_domain.rb +38 -0
  22. data/lib/kono_epp_client/commands/update_contact.rb +116 -0
  23. data/lib/kono_epp_client/commands/update_domain.rb +117 -0
  24. data/lib/kono_epp_client/dns_sec/add.rb +13 -0
  25. data/lib/kono_epp_client/dns_sec/ds_data.rb +65 -0
  26. data/lib/kono_epp_client/dns_sec/rem.rb +14 -0
  27. data/lib/kono_epp_client/dns_sec/rem_all.rb +10 -0
  28. data/lib/kono_epp_client/exceptions/error_response.rb +19 -0
  29. data/lib/kono_epp_client/exceptions.rb +15 -0
  30. data/lib/kono_epp_client/requires_parameters.rb +16 -0
  31. data/lib/kono_epp_client/server.rb +311 -0
  32. data/lib/kono_epp_client/version.rb +3 -0
  33. data/lib/kono_epp_client.rb +4 -16
  34. data/spec/eager_load_spec.rb +6 -0
  35. data/spec/factories/ds_data.rb +12 -0
  36. data/spec/fixtures/dns_sec/add.xml +14 -0
  37. data/spec/fixtures/dns_sec/ds_data.xml +6 -0
  38. data/spec/fixtures/dns_sec/rem.xml +14 -0
  39. data/spec/fixtures/dns_sec/rem_all.xml +3 -0
  40. data/spec/fixtures/login_response.xml +23 -0
  41. data/spec/fixtures/login_response_for_dnssec.xml +25 -0
  42. data/spec/fixtures/snapshots/kono_epp_client/commands/create_domain/_create_with_ds_data_build_extensions.xml.snap +39 -0
  43. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_login.xml.snap +7 -0
  44. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_tags_extensions_with_value_xml.xml.snap +14 -0
  45. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_tags_lang_with_value_xml.xml.snap +11 -0
  46. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_tags_new_password_with_value_xml.xml.snap +9 -0
  47. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_tags_services_with_value_xml.xml.snap +12 -0
  48. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_tags_version_with_value_xml.xml.snap +11 -0
  49. data/spec/fixtures/snapshots/kono_epp_client/commands/login/_with_id_and_password_login.xml.snap +10 -0
  50. data/spec/fixtures/snapshots/kono_epp_client/commands/update_domain/_update_DnsSec_build_extensions.xml.snap +30 -0
  51. data/spec/{epp/epp_command/kono_epp_check_contacts_spec.rb → lib/commands/check_contacts_spec.rb} +1 -1
  52. data/spec/{epp/epp_command/kono_epp_check_domains_spec.rb → lib/commands/check_domains_spec.rb} +1 -1
  53. data/spec/{epp/kono_epp_command_spec.rb → lib/commands/command_spec.rb} +2 -2
  54. data/spec/lib/commands/create_domain_spec.rb +50 -0
  55. data/spec/lib/commands/login_spec.rb +87 -0
  56. data/spec/{epp/epp_command/kono_epp_transfer_domain_spec.rb → lib/commands/transfer_domain_spec.rb} +1 -1
  57. data/spec/{epp/epp_command/kono_epp_update_domain_spec.rb → lib/commands/update_domain_spec.rb} +20 -4
  58. data/spec/lib/dns_sec/add_spec.rb +14 -0
  59. data/spec/lib/dns_sec/ds_data_spec.rb +43 -0
  60. data/spec/lib/dns_sec/rem_all_spec.rb +11 -0
  61. data/spec/lib/dns_sec/rem_spec.rb +14 -0
  62. data/spec/lib/server_spec.rb +304 -0
  63. data/spec/spec_helper.rb +6 -2
  64. data/spec/support/context.rb +1 -1
  65. data/spec/support/factory_bot.rb +8 -0
  66. data/spec/support/fixtures.rb +16 -0
  67. data/spec/support/matchers.rb +14 -0
  68. data/spec/support/parametric.rb +1 -0
  69. data/spec/support/snapshot.rb +13 -3
  70. data/spec/support/superdiff.rb +1 -0
  71. metadata +178 -58
  72. data/lib/epp/epp_command/check_contacts.rb +0 -17
  73. data/lib/epp/epp_command/create_contact.rb +0 -87
  74. data/lib/epp/epp_command/create_domain.rb +0 -44
  75. data/lib/epp/epp_command/delete_contact.rb +0 -15
  76. data/lib/epp/epp_command/delete_domain.rb +0 -14
  77. data/lib/epp/epp_command/hello.rb +0 -13
  78. data/lib/epp/epp_command/info_contact.rb +0 -15
  79. data/lib/epp/epp_command/info_domain.rb +0 -14
  80. data/lib/epp/epp_command/login.rb +0 -79
  81. data/lib/epp/epp_command/logout.rb +0 -10
  82. data/lib/epp/epp_command/poll.rb +0 -16
  83. data/lib/epp/epp_command/transfer_domain.rb +0 -36
  84. data/lib/epp/epp_command/update_contact.rb +0 -115
  85. data/lib/epp/epp_command/update_domain.rb +0 -104
  86. data/lib/epp/epp_command.rb +0 -15
  87. data/lib/epp/exceptions.rb +0 -28
  88. data/lib/epp/server.rb +0 -295
  89. data/lib/epp/transport/tcp.rb +0 -93
  90. data/lib/require_parameters.rb +0 -14
  91. data/spec/epp/epp_command/kono_epp_create_domain_spec.rb +0 -37
  92. /data/lib/{epp/transport/http.rb → kono_epp_client/transport/http_transport.rb} +0 -0
  93. /data/lib/{epp → kono_epp_client}/transport.rb +0 -0
  94. /data/spec/fixtures/snapshots/{kono_epp_check_contacts → kono_epp_client/commands/check_contacts}/_construct.xml.snap +0 -0
  95. /data/spec/fixtures/snapshots/{kono_epp_check_domains → kono_epp_client/commands/check_domains}/_construct.xml.snap +0 -0
  96. /data/spec/fixtures/snapshots/{kono_epp_create_domain → kono_epp_client/commands/create_domain}/_create.xml.snap +0 -0
  97. /data/spec/fixtures/snapshots/{kono_epp_transfer_domain → kono_epp_client/commands/transfer_domain}/_con_extension_construct.xml.snap +0 -0
  98. /data/spec/fixtures/snapshots/{kono_epp_transfer_domain → kono_epp_client/commands/transfer_domain}/_construct.xml.snap +0 -0
  99. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_restore_esiste_l'estensione_di_restore.xml.snap +0 -0
  100. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_update_auth_info_cambia_AUTH_INFO.xml.snap +0 -0
  101. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_update_auth_info_con_nuovo_registrant_cambia_REGISTRANT.xml.snap +0 -0
  102. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_update_contacts_cambia_ADMIN_TECH.xml.snap +0 -0
  103. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_update_nameservers_aggiunge_e_rimuove_ns.xml.snap +0 -0
  104. /data/spec/fixtures/snapshots/{kono_epp_update_domain → kono_epp_client/commands/update_domain}/_update_status_cambia_status.xml.snap +0 -0
@@ -0,0 +1,116 @@
1
+ module KonoEppClient::Commands
2
+ class UpdateContact < Command
3
+
4
+ # TODO: Add and remove fields
5
+ def initialize(options)
6
+ super(nil, nil)
7
+
8
+ command = root.elements['command']
9
+
10
+ update = command.add_element "update"
11
+
12
+ contact_update = update.add_element("contact:update", {"xmlns:contact" => "urn:ietf:params:xml:ns:contact-1.0",
13
+ "xsi:schemaLocation" => "urn:ietf:params:xml:ns:contact-1.0 contact-1.0.xsd"})
14
+
15
+ id = contact_update.add_element "contact:id"
16
+ id.text = options[:id]
17
+
18
+ contact_chg = contact_update.add_element "contact:chg"
19
+
20
+ unless options[:name].blank? \
21
+ and options[:organization].blank? \
22
+ and options[:address].blank? \
23
+ and options[:city].blank? \
24
+ and options[:state].blank? \
25
+ and options[:postal_code].blank? \
26
+ and options[:country].blank?
27
+
28
+ postal_info = contact_chg.add_element "contact:postalInfo", {"type" => "loc"}
29
+
30
+ unless options[:name].blank?
31
+ name = postal_info.add_element "contact:name"
32
+ name.text = options[:name]
33
+ end
34
+
35
+ unless options[:organization].blank?
36
+ organization = postal_info.add_element "contact:org"
37
+ organization.text = options[:organization]
38
+ end
39
+
40
+ # NOTE: city and country are REQUIRED
41
+ unless options[:address].blank? \
42
+ and options[:city].blank? \
43
+ and options[:state].blank? \
44
+ and options[:postal_code].blank? \
45
+ and options[:country].blank?
46
+
47
+ addr = postal_info.add_element "contact:addr"
48
+
49
+ unless options[:address].blank?
50
+ street = addr.add_element "contact:street"
51
+ street.text = options[:address]
52
+ end
53
+
54
+ city = addr.add_element "contact:city"
55
+ city.text = options[:city]
56
+
57
+ unless options[:state].blank?
58
+ state = addr.add_element "contact:sp"
59
+ state.text = options[:state]
60
+ end
61
+
62
+ unless options[:postal_code].blank?
63
+ postal_code = addr.add_element "contact:pc"
64
+ postal_code.text = options[:postal_code]
65
+ end
66
+
67
+ country_code = addr.add_element "contact:cc"
68
+ country_code.text = options[:country]
69
+ end
70
+ end
71
+
72
+ if options[:voice]
73
+ voice = contact_chg.add_element "contact:voice"
74
+ voice.text = options[:voice]
75
+ end
76
+
77
+ if options[:fax]
78
+ fax = contact_chg.add_element "contact:fax"
79
+ fax.text = options[:fax]
80
+ end
81
+
82
+ if options[:email]
83
+ email = contact_chg.add_element "contact:email"
84
+ email.text = options[:email]
85
+ end
86
+
87
+ unless not options.has_key?(:publish) \
88
+ and options[:nationality].blank? \
89
+ and options[:entity_type].blank? \
90
+ and options[:reg_code].blank?
91
+
92
+ # FIXME
93
+ extension = command.add_element "extension"
94
+ extension_update = extension.add_element "extcon:update", {"xmlns:extcon" => 'http://www.nic.it/ITNIC-EPP/extcon-1.0',
95
+ "xsi:schemaLocation" => 'http://www.nic.it/ITNIC-EPP/extcon-1.0 extcon-1.0.xsd'}
96
+ if options.has_key?(:publish)
97
+ publish = extension_update.add_element "extcon:consentForPublishing"
98
+ publish.text = options[:publish]
99
+ end
100
+
101
+ if options[:becomes_registrant]
102
+ extcon_registrant = extension_update.add_element "extcon:registrant"
103
+
104
+ extcon_nationality = extcon_registrant.add_element "extcon:nationalityCode"
105
+ extcon_nationality.text = options[:nationality]
106
+
107
+ extcon_entity = extcon_registrant.add_element "extcon:entityType"
108
+ extcon_entity.text = options[:entity_type]
109
+
110
+ extcon_regcode = extcon_registrant.add_element "extcon:regCode"
111
+ extcon_regcode.text = options[:reg_code]
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,117 @@
1
+ module KonoEppClient::Commands
2
+ class UpdateDomain < Command
3
+ def initialize(options)
4
+ super(nil, nil)
5
+
6
+ command = root.elements['command']
7
+ update = command.add_element("update")
8
+
9
+ domain_update = update.add_element("domain:update", {"xmlns:domain" => "urn:ietf:params:xml:ns:domain-1.0",
10
+ "xsi:schemaLocation" => "urn:ietf:params:xml:ns:domain-1.0 domain-1.0.xsd"})
11
+
12
+ name = domain_update.add_element "domain:name"
13
+ name.text = options[:name]
14
+
15
+ if not options[:add_nameservers].blank? or options[:add_admin] or options[:add_tech] or options[:add_status]
16
+ # <domain:add>
17
+ domain_add = domain_update.add_element "domain:add"
18
+
19
+ unless options[:add_nameservers].blank?
20
+ domain_add_ns = domain_add.add_element "domain:ns"
21
+
22
+ options[:add_nameservers].each do |ns|
23
+ host_attr = domain_add_ns.add_element "domain:hostAttr"
24
+ host_name = host_attr.add_element "domain:hostName"
25
+
26
+ host_name.text = ns[0]
27
+
28
+ # FIXME IPv6
29
+ if ns[1]
30
+ host_addr = host_attr.add_element "domain:hostAddr", {"ip" => "v4"}
31
+ host_addr.text = ns[1]
32
+ end
33
+ end
34
+ end
35
+
36
+ if options[:add_admin]
37
+ domain_contact = domain_add.add_element "domain:contact", {"type" => "admin"}
38
+ domain_contact.text = options[:add_admin]
39
+ end
40
+
41
+ if options[:add_status]
42
+ domain_add.add_element "domain:status", {"s" => options[:add_status]}
43
+ end
44
+
45
+ if options[:add_tech]
46
+ domain_contact = domain_add.add_element "domain:contact", {"type" => "tech"}
47
+ domain_contact.text = options[:add_tech]
48
+ end
49
+ end
50
+
51
+ if not options[:remove_nameservers].blank? or options[:remove_admin] or options[:remove_tech] or options[:remove_status]
52
+ # <domain:rem>
53
+ domain_remove = domain_update.add_element "domain:rem"
54
+
55
+ unless options[:remove_nameservers].blank?
56
+ domain_remove_ns = domain_remove.add_element "domain:ns"
57
+
58
+ options[:remove_nameservers].each do |ns_name|
59
+ host_attr = domain_remove_ns.add_element "domain:hostAttr"
60
+ host_name = host_attr.add_element "domain:hostName"
61
+
62
+ host_name.text = ns_name
63
+ end
64
+ end
65
+
66
+ if options[:remove_admin]
67
+ domain_contact = domain_remove.add_element "domain:contact", {"type" => "admin"}
68
+ domain_contact.text = options[:remove_admin]
69
+ end
70
+
71
+ if options[:remove_status]
72
+ domain_remove.add_element "domain:status", {"s" => options[:remove_status]}
73
+ end
74
+
75
+ if options[:remove_tech]
76
+ domain_contact = domain_remove.add_element "domain:contact", {"type" => "tech"}
77
+ domain_contact.text = options[:remove_tech]
78
+ end
79
+ end
80
+
81
+ # <domain:chg>
82
+ if options[:auth_info]
83
+ domain_change = domain_update.add_element "domain:chg"
84
+
85
+ if options[:registrant]
86
+ domain_registrant = domain_change.add_element "domain:registrant"
87
+ domain_registrant.text = options[:registrant]
88
+ end
89
+
90
+ domain_authinfo = domain_change.add_element "domain:authInfo"
91
+
92
+ domain_pw = domain_authinfo.add_element "domain:pw"
93
+ domain_pw.text = options[:auth_info]
94
+ end
95
+
96
+ if options[:restore]
97
+ command.add_element("extension").tap do |ext|
98
+ ext.add_element("rgp:update", {"xmlns:rgp" => "urn:ietf:params:xml:ns:rgp-1.0",
99
+ "xsi:schemaLocation" => "urn:ietf:params:xml:ns:rgp-1.0 rgp-1.0.xsd"}).
100
+ add_element("rgp:restore", {"op" => "request"})
101
+ end
102
+ end
103
+ # TODO: Registrant
104
+
105
+ if options[:dns_sec_data] and options[:dns_sec_data].any?
106
+ extension = command.elements['extension'] || command.add_element("extension")
107
+
108
+ create_list = extension.add_element("secDNS:update", {"xmlns:secDNS"=> "urn:ietf:params:xml:ns:secDNS-1.1"})
109
+
110
+ options[:dns_sec_data].each do |d|
111
+ create_list.add_element(d)
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KonoEppClient::DnsSec
4
+ class Add < REXML::Element
5
+ def initialize(*ds_datas)
6
+ super("secDNS:add")
7
+
8
+ ds_datas.each do |ds_data|
9
+ self.add_element ds_data
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ module KonoEppClient::DnsSec
3
+ class DsData < REXML::Element
4
+
5
+ attr_accessor :key_tag, :alg, :digest_type, :digest
6
+
7
+ ALG = {
8
+ :dsa_sha_1 => 3,
9
+ :rsa_sha_1 => 5,
10
+ :dsa_nsec_3_sha_1 => 6,
11
+ :rsasha_1_nsec_3_sha_1 => 7,
12
+ :rsa_sha_256 => 8,
13
+ :rsa_sha_512 => 10,
14
+ :ecc_gost => 12,
15
+ :ecdsap_256_sha_256 => 13,
16
+ :ecdsap_384_sha_384 => 14
17
+ }.freeze
18
+ DIGEST_TYPES = {
19
+ :sha_1 => 1,
20
+ :sha_256 => 2,
21
+ :gost_r_34_11_94 => 3,
22
+ :sha_384 => 4
23
+ }.freeze
24
+
25
+ ##
26
+ # Inizializzazione di un DsData
27
+ #
28
+ # @param [Integer] key_tag 0<=X<=65535
29
+ # @param [Symbol] alg
30
+ # :dsa_sha_1 => 3 (DSA/SHA-1)
31
+ # :rsa_sha_1 => 5 (RSA/SHA-1)
32
+ # :dsa_nsec_3_sha_1 => 6 (DSA-NSEC3-SHA1)
33
+ # :rsasha_1_nsec_3_sha_1 => 7 (RSASHA1-NSEC3-SHA1)
34
+ # :rsa_sha_256 => 8 (RSA/SHA-256)
35
+ # :rsa_sha_512 => 10 (RSA/SHA-512)
36
+ # :ecc_gost => 12 (ECC-GOST)
37
+ # :ecdsap_256_sha_256 => 13 (ECDSAP256SHA256)
38
+ # :ecdsap_384_sha_384 => 14 (ECDSAP384SHA384)
39
+ # @param [Symbol] digest_type
40
+ # :sha_1 => 1 (SHA-1)
41
+ # :sha_256 => 2 (SHA-256)
42
+ # :gost_r_34_11_94 => 3 (GOST R 34.11-94)
43
+ # :sha_384 => 4 (SHA-384)
44
+ # @param [String] digest
45
+ def initialize(key_tag, alg, digest_type, digest)
46
+ key_tag = key_tag.to_i
47
+ @alg = ALG[alg] || raise("Invalid alg #{alg}")
48
+ @digest = digest
49
+ if (0..65535).include?(key_tag)
50
+ @key_tag = key_tag
51
+ else
52
+ raise "Invalid key tag #{key_tag}, should be 0<=key_tag<=65535"
53
+ end
54
+ @digest_type = DIGEST_TYPES[digest_type] || raise("Invalid digest type #{digest_type}")
55
+
56
+ super("secDNS:dsData")
57
+ self.add_element("secDNS:keyTag").text = @key_tag
58
+ self.add_element("secDNS:alg").text = @alg
59
+ self.add_element("secDNS:digestType").text = @digest_type
60
+ self.add_element("secDNS:digest").text = @digest
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KonoEppClient::DnsSec
4
+ class Rem < REXML::Element
5
+ def initialize(*ds_datas)
6
+ super("secDNS:rem")
7
+
8
+ ds_datas.each do |ds_data|
9
+ self.add_element ds_data
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KonoEppClient::DnsSec
4
+ class RemAll < REXML::Element
5
+ def initialize
6
+ super("secDNS:rem")
7
+ self.add_element("secDNS:all").text="true"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KonoEppClient::Exceptions
4
+ class ErrorResponse < StandardError #:nodoc:
5
+ attr_accessor :response_xml, :response_code, :reason_code, :message
6
+
7
+ # Generic EPP exception. Accepts a response code and a message
8
+ def initialize(attributes = {})
9
+ @response_xml = attributes[:xml]
10
+ @response_code = attributes[:response_code]
11
+ @reason_code = attributes[:reason_code]
12
+ @message = attributes[:message]
13
+ end
14
+
15
+ def to_s
16
+ "#{@message} (reason: #{@reason_code} code: #{@response_code})"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module KonoEppClient::Exceptions
2
+ class AuthenticationPasswordExpired < ErrorResponse; end
3
+
4
+ class LoginNeeded < ErrorResponse; end
5
+
6
+ ##
7
+ # Errore NIC:
8
+ # 2304=Object status prohibits operation 9022=Domain has status clientTransferProhibited
9
+ class DomainHasStatusCliTransProhibited < ErrorResponse; end
10
+
11
+ ##
12
+ # Errore NIC:
13
+ # 2304=Object status prohibits operation 9026=Domain has status clientUpdateProhibited
14
+ class DomainHasStatusClientUpdateProhibited < ErrorResponse; end
15
+ end
@@ -0,0 +1,16 @@
1
+ module KonoEppClient
2
+ module RequiresParameters #:nodoc:
3
+ def requires!(hash, *params)
4
+ params.each do |param|
5
+ if param.is_a?(Array)
6
+ raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
7
+
8
+ valid_options = param[1..-1]
9
+ raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:connector => 'or')}") unless valid_options.include?(hash[param.first])
10
+ else
11
+ raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end