launchdarkly-server-sdk 8.11.3-java → 8.12.0-java

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c1f4bb6ab8a6d7fbd4344391172a88308a8e60d7fa2a30ca88a020b1aa1fbef
4
- data.tar.gz: bd10f765d90613ae5369c325f8c044a448cec66324346d86c4a16edf57a96636
3
+ metadata.gz: 1725cd5066df8e5072abb09c606e0bda2784fb2b16640cdf6b557a4119e2356c
4
+ data.tar.gz: efbba66e2391376fdd945a7e26bbb429e121a127b267b1f14d7685d7f2098a3f
5
5
  SHA512:
6
- metadata.gz: 86c8930883516942ee113813bcdc0ad3bdd241d07c3c8e4ef2d0dfc21467ecbbb04a403ed8b162e8cfc5307acfd71c9571013fd5b918f594fc46b447950e40c2
7
- data.tar.gz: ba1e21ae402cd581eb1e790e19a2f6aed71f17f157a5e93d47fe91992574f31ae4803b5a02f32c3b55b02fde6cf95b5bb94d000e288bf9ccc189881bffa0c289
6
+ metadata.gz: aecbdb3e07abe04e7993fd7967ee10376bbb9e19e70558858fd11b16ee5a5f56327005c1b76df628bf3a5d552b6462b3d2e57a1d8dee0477e18454a0d3e30a66
7
+ data.tar.gz: 56b6cfad3066a766c76fdf70ab3f204778bf208cfda316eed9ba8bbf5106c8ccceb7c3fa436d6af8045093b61d741791a087df78cca21d937c47bb2eb018cf14
@@ -1,5 +1,8 @@
1
1
  require "logger"
2
2
  require "ldclient-rb/impl/cache_store"
3
+ require "ldclient-rb/impl/data_system/http_config_options"
4
+ require "ldclient-rb/impl/data_system/polling"
5
+ require "ldclient-rb/impl/data_system/streaming"
3
6
 
4
7
  module LaunchDarkly
5
8
  #
@@ -45,7 +48,7 @@ module LaunchDarkly
45
48
  # @option opts [Hash] :application See {#application}
46
49
  # @option opts [String] :payload_filter_key See {#payload_filter_key}
47
50
  # @option opts [Boolean] :omit_anonymous_contexts See {#omit_anonymous_contexts}
48
- # @option opts [DataSystemConfig] :datasystem_config See {#datasystem_config}
51
+ # @option opts [DataSystemConfig] :data_system_config See {#data_system_config}
49
52
  # @option hooks [Array<Interfaces::Hooks::Hook]
50
53
  # @option plugins [Array<Interfaces::Plugins::Plugin]
51
54
  #
@@ -84,7 +87,7 @@ module LaunchDarkly
84
87
  @hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
85
88
  @plugins = (opts[:plugins] || []).keep_if { |plugin| plugin.is_a? Interfaces::Plugins::Plugin }
86
89
  @omit_anonymous_contexts = opts.has_key?(:omit_anonymous_contexts) && opts[:omit_anonymous_contexts]
87
- @datasystem_config = opts[:datasystem_config]
90
+ @data_system_config = opts[:data_system_config]
88
91
  @data_source_update_sink = nil
89
92
  @instance_id = nil
90
93
  end
@@ -440,7 +443,7 @@ module LaunchDarkly
440
443
  #
441
444
  # @return [DataSystemConfig, nil]
442
445
  #
443
- attr_reader :datasystem_config
446
+ attr_reader :data_system_config
444
447
 
445
448
 
446
449
  #
@@ -465,7 +468,7 @@ module LaunchDarkly
465
468
  # @return [String] "https://sdk.launchdarkly.com"
466
469
  #
467
470
  def self.default_base_uri
468
- "https://sdk.launchdarkly.com"
471
+ Impl::DataSystem::PollingDataSourceBuilder::DEFAULT_BASE_URI
469
472
  end
470
473
 
471
474
  #
@@ -473,7 +476,7 @@ module LaunchDarkly
473
476
  # @return [String] "https://stream.launchdarkly.com"
474
477
  #
475
478
  def self.default_stream_uri
476
- "https://stream.launchdarkly.com"
479
+ Impl::DataSystem::StreamingDataSourceBuilder::DEFAULT_BASE_URI
477
480
  end
478
481
 
479
482
  #
@@ -505,7 +508,7 @@ module LaunchDarkly
505
508
  # @return [Float] 10
506
509
  #
507
510
  def self.default_read_timeout
508
- 10
511
+ Impl::DataSystem::HttpConfigOptions::DEFAULT_READ_TIMEOUT
509
512
  end
510
513
 
511
514
  #
@@ -513,7 +516,7 @@ module LaunchDarkly
513
516
  # @return [Float] 1
514
517
  #
515
518
  def self.default_initial_reconnect_delay
516
- 1
519
+ Impl::DataSystem::StreamingDataSourceBuilder::DEFAULT_INITIAL_RECONNECT_DELAY
517
520
  end
518
521
 
519
522
  #
@@ -521,7 +524,7 @@ module LaunchDarkly
521
524
  # @return [Float] 2
522
525
  #
523
526
  def self.default_connect_timeout
524
- 2
527
+ Impl::DataSystem::HttpConfigOptions::DEFAULT_CONNECT_TIMEOUT
525
528
  end
526
529
 
527
530
  #
@@ -575,7 +578,7 @@ module LaunchDarkly
575
578
  # @return [Float] 30
576
579
  #
577
580
  def self.default_poll_interval
578
- 30
581
+ Impl::DataSystem::PollingDataSourceBuilder::DEFAULT_POLL_INTERVAL
579
582
  end
580
583
 
581
584
  #
@@ -699,35 +702,29 @@ module LaunchDarkly
699
702
  #
700
703
  class DataSystemConfig
701
704
  #
702
- # @param initializers [Array<Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>, nil] The (optional) array of builder procs
703
- # @param primary_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil] The (optional) builder proc for primary synchronizer
704
- # @param secondary_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil] The (optional) builder proc for secondary synchronizer
705
+ # @param initializers [Array<#build(String, Config)>, nil] The (optional) array of builders
706
+ # @param synchronizers [Array<#build(String, Config)>, nil] The (optional) array of synchronizer builders
705
707
  # @param data_store_mode [Symbol] The (optional) data store mode
706
708
  # @param data_store [LaunchDarkly::Interfaces::FeatureStore, nil] The (optional) data store
707
- # @param fdv1_fallback_synchronizer [Proc(Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
708
- # The (optional) builder proc for FDv1-compatible fallback synchronizer
709
+ # @param fdv1_fallback_synchronizer [#build(String, Config), nil]
710
+ # The (optional) builder for FDv1-compatible fallback synchronizer
709
711
  #
710
- def initialize(initializers: nil, primary_synchronizer: nil, secondary_synchronizer: nil,
712
+ def initialize(initializers: nil, synchronizers: nil,
711
713
  data_store_mode: LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_ONLY, data_store: nil, fdv1_fallback_synchronizer: nil)
712
714
  @initializers = initializers
713
- @primary_synchronizer = primary_synchronizer
714
- @secondary_synchronizer = secondary_synchronizer
715
+ @synchronizers = synchronizers
715
716
  @data_store_mode = data_store_mode
716
717
  @data_store = data_store
717
718
  @fdv1_fallback_synchronizer = fdv1_fallback_synchronizer
718
719
  end
719
720
 
720
- # The initializers for the data system. Each proc takes sdk_key and Config and returns an Initializer.
721
- # @return [Array<Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>, nil]
721
+ # The initializer builders for the data system. Each builder responds to build(sdk_key, config) and returns an Initializer.
722
+ # @return [Array<#build(String, Config)>, nil]
722
723
  attr_reader :initializers
723
724
 
724
- # The primary synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
725
- # @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
726
- attr_reader :primary_synchronizer
727
-
728
- # The secondary synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
729
- # @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
730
- attr_reader :secondary_synchronizer
725
+ # The synchronizer builders for the data system. Each builder responds to build(sdk_key, config) and returns a Synchronizer.
726
+ # @return [Array<#build(String, Config)>, nil]
727
+ attr_reader :synchronizers
731
728
 
732
729
  # The data store mode.
733
730
  # @return [Symbol]
@@ -737,8 +734,8 @@ module LaunchDarkly
737
734
  # @return [LaunchDarkly::Interfaces::FeatureStore, nil]
738
735
  attr_reader :data_store
739
736
 
740
- # The FDv1-compatible fallback synchronizer builder. Takes sdk_key and Config and returns a Synchronizer.
741
- # @return [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
737
+ # The FDv1-compatible fallback synchronizer builder. Responds to build(sdk_key, config) and returns a Synchronizer.
738
+ # @return [#build(String, Config), nil]
742
739
  attr_reader :fdv1_fallback_synchronizer
743
740
  end
744
741
  end
@@ -3,6 +3,7 @@
3
3
  require 'ldclient-rb/interfaces/data_system'
4
4
  require 'ldclient-rb/config'
5
5
  require 'ldclient-rb/impl/data_system/polling'
6
+ require 'ldclient-rb/impl/data_system/streaming'
6
7
 
7
8
  module LaunchDarkly
8
9
  #
@@ -17,8 +18,7 @@ module LaunchDarkly
17
18
  class ConfigBuilder
18
19
  def initialize
19
20
  @initializers = nil
20
- @primary_synchronizer = nil
21
- @secondary_synchronizer = nil
21
+ @synchronizers = nil
22
22
  @fdv1_fallback_synchronizer = nil
23
23
  @data_store_mode = LaunchDarkly::Interfaces::DataSystem::DataStoreMode::READ_ONLY
24
24
  @data_store = nil
@@ -27,8 +27,8 @@ module LaunchDarkly
27
27
  #
28
28
  # Sets the initializers for the data system.
29
29
  #
30
- # @param initializers [Array<Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Initializer>]
31
- # Array of builder procs that take sdk_key and Config and return an Initializer
30
+ # @param initializers [Array<#build(String, Config)>]
31
+ # Array of builders that respond to build(sdk_key, config) and return an Initializer
32
32
  # @return [ConfigBuilder] self for chaining
33
33
  #
34
34
  def initializers(initializers)
@@ -39,14 +39,12 @@ module LaunchDarkly
39
39
  #
40
40
  # Sets the synchronizers for the data system.
41
41
  #
42
- # @param primary [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer] Builder proc that takes sdk_key and Config and returns the primary Synchronizer
43
- # @param secondary [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer, nil]
44
- # Builder proc that takes sdk_key and Config and returns the secondary Synchronizer
42
+ # @param synchronizers [Array<#build(String, Config)>]
43
+ # Array of builders that respond to build(sdk_key, config) and return a Synchronizer
45
44
  # @return [ConfigBuilder] self for chaining
46
45
  #
47
- def synchronizers(primary, secondary = nil)
48
- @primary_synchronizer = primary
49
- @secondary_synchronizer = secondary
46
+ def synchronizers(synchronizers)
47
+ @synchronizers = synchronizers
50
48
  self
51
49
  end
52
50
 
@@ -54,8 +52,7 @@ module LaunchDarkly
54
52
  # Configures the SDK with a fallback synchronizer that is compatible with
55
53
  # the Flag Delivery v1 API.
56
54
  #
57
- # @param fallback [Proc(String, Config) => LaunchDarkly::Interfaces::DataSystem::Synchronizer]
58
- # Builder proc that takes sdk_key and Config and returns the fallback Synchronizer
55
+ # @param fallback [#build(String, Config)] Builder that responds to build(sdk_key, config) and returns the fallback Synchronizer
59
56
  # @return [ConfigBuilder] self for chaining
60
57
  #
61
58
  def fdv1_compatible_synchronizer(fallback)
@@ -80,17 +77,11 @@ module LaunchDarkly
80
77
  # Builds the data system configuration.
81
78
  #
82
79
  # @return [DataSystemConfig]
83
- # @raise [ArgumentError] if configuration is invalid
84
80
  #
85
81
  def build
86
- if @secondary_synchronizer && @primary_synchronizer.nil?
87
- raise ArgumentError, "Primary synchronizer must be set if secondary is set"
88
- end
89
-
90
82
  DataSystemConfig.new(
91
83
  initializers: @initializers,
92
- primary_synchronizer: @primary_synchronizer,
93
- secondary_synchronizer: @secondary_synchronizer,
84
+ synchronizers: @synchronizers,
94
85
  data_store_mode: @data_store_mode,
95
86
  data_store: @data_store,
96
87
  fdv1_fallback_synchronizer: @fdv1_fallback_synchronizer
@@ -99,43 +90,36 @@ module LaunchDarkly
99
90
  end
100
91
 
101
92
  #
102
- # Returns a builder proc for creating a polling data source.
93
+ # Returns a builder for creating a polling data source.
103
94
  # This is a building block that can be used with {ConfigBuilder#initializers}
104
95
  # or {ConfigBuilder#synchronizers} to create custom data system configurations.
105
96
  #
106
- # @return [Proc] A proc that takes (sdk_key, config) and returns a polling data source
97
+ # @return [LaunchDarkly::Impl::DataSystem::PollingDataSourceBuilder]
107
98
  #
108
99
  def self.polling_ds_builder
109
- lambda do |sdk_key, config|
110
- LaunchDarkly::Impl::DataSystem::PollingDataSourceBuilder.new(sdk_key, config).build
111
- end
100
+ LaunchDarkly::Impl::DataSystem::PollingDataSourceBuilder.new
112
101
  end
113
102
 
114
103
  #
115
- # Returns a builder proc for creating an FDv1 fallback polling data source.
104
+ # Returns a builder for creating an FDv1 fallback polling data source.
116
105
  # This is a building block that can be used with {ConfigBuilder#fdv1_compatible_synchronizer}
117
106
  # to provide FDv1 compatibility in custom data system configurations.
118
107
  #
119
- # @return [Proc] A proc that takes (sdk_key, config) and returns an FDv1 polling data source
108
+ # @return [LaunchDarkly::Impl::DataSystem::FDv1PollingDataSourceBuilder]
120
109
  #
121
110
  def self.fdv1_fallback_ds_builder
122
- lambda do |sdk_key, config|
123
- LaunchDarkly::Impl::DataSystem::FDv1PollingDataSourceBuilder.new(sdk_key, config).build
124
- end
111
+ LaunchDarkly::Impl::DataSystem::FDv1PollingDataSourceBuilder.new
125
112
  end
126
113
 
127
114
  #
128
- # Returns a builder proc for creating a streaming data source.
115
+ # Returns a builder for creating a streaming data source.
129
116
  # This is a building block that can be used with {ConfigBuilder#synchronizers}
130
117
  # to create custom data system configurations.
131
118
  #
132
- # @return [Proc] A proc that takes (sdk_key, config) and returns a streaming data source
119
+ # @return [LaunchDarkly::Impl::DataSystem::StreamingDataSourceBuilder]
133
120
  #
134
121
  def self.streaming_ds_builder
135
- # TODO(fdv2): Implement streaming data source builder
136
- lambda do |_sdk_key, _config|
137
- raise NotImplementedError, "Streaming data source not yet implemented for FDv2"
138
- end
122
+ LaunchDarkly::Impl::DataSystem::StreamingDataSourceBuilder.new
139
123
  end
140
124
 
141
125
  #
@@ -159,7 +143,7 @@ module LaunchDarkly
159
143
 
160
144
  builder = ConfigBuilder.new
161
145
  builder.initializers([polling_builder])
162
- builder.synchronizers(streaming_builder, polling_builder)
146
+ builder.synchronizers([streaming_builder, polling_builder])
163
147
  builder.fdv1_compatible_synchronizer(fallback)
164
148
 
165
149
  builder
@@ -177,7 +161,7 @@ module LaunchDarkly
177
161
  fallback = fdv1_fallback_ds_builder
178
162
 
179
163
  builder = ConfigBuilder.new
180
- builder.synchronizers(streaming_builder)
164
+ builder.synchronizers([streaming_builder])
181
165
  builder.fdv1_compatible_synchronizer(fallback)
182
166
 
183
167
  builder
@@ -195,7 +179,7 @@ module LaunchDarkly
195
179
  fallback = fdv1_fallback_ds_builder
196
180
 
197
181
  builder = ConfigBuilder.new
198
- builder.synchronizers(polling_builder)
182
+ builder.synchronizers([polling_builder])
199
183
  builder.fdv1_compatible_synchronizer(fallback)
200
184
 
201
185
  builder
@@ -156,7 +156,7 @@ module LaunchDarkly
156
156
  @inbox_full = Concurrent::AtomicBoolean.new(false)
157
157
 
158
158
  event_sender = (test_properties || {})[:event_sender] ||
159
- Impl::EventSender.new(sdk_key, config, client || Impl::Util.new_http_client(config.events_uri, config))
159
+ Impl::EventSender.new(sdk_key, config)
160
160
 
161
161
  @timestamp_fn = (test_properties || {})[:timestamp_fn] || proc { Impl::Util.current_time_millis }
162
162
  @omit_anonymous_contexts = config.omit_anonymous_contexts
@@ -1,5 +1,6 @@
1
1
  require "ldclient-rb/impl/model/serialization"
2
2
  require "ldclient-rb/impl/util"
3
+ require "ldclient-rb/impl/data_system/http_config_options"
3
4
 
4
5
  require "concurrent/atomics"
5
6
  require "json"
@@ -26,7 +27,13 @@ module LaunchDarkly
26
27
  def initialize(sdk_key, config)
27
28
  @sdk_key = sdk_key
28
29
  @config = config
29
- @http_client = Impl::Util.new_http_client(config.base_uri, config)
30
+ @http_config = DataSystem::HttpConfigOptions.new(
31
+ base_uri: config.base_uri,
32
+ socket_factory: config.socket_factory,
33
+ read_timeout: config.read_timeout,
34
+ connect_timeout: config.connect_timeout
35
+ )
36
+ @http_client = Impl::Util.new_http_client(@http_config)
30
37
  .use(:auto_inflate)
31
38
  .headers("Accept-Encoding" => "gzip")
32
39
  @cache = @config.cache_store
@@ -48,7 +55,7 @@ module LaunchDarkly
48
55
 
49
56
  def make_request(path)
50
57
  uri = URI(
51
- Util.add_payload_filter_key(@config.base_uri + path, @config)
58
+ Util.add_payload_filter_key(@http_config.base_uri + path, @config)
52
59
  )
53
60
  headers = {}
54
61
  Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
@@ -54,6 +54,11 @@ module LaunchDarkly
54
54
  new_state = LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
55
55
  end
56
56
 
57
+ # Special handling: You can't go back to INITIALIZING after being anything else
58
+ if new_state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING && !old_status.state.nil?
59
+ new_state = old_status.state
60
+ end
61
+
57
62
  # No change if state is the same and no error
58
63
  return if new_state == old_status.state && new_error.nil?
59
64
 
@@ -28,7 +28,7 @@ module LaunchDarkly
28
28
  items_of_kind = @items[kind]
29
29
  return nil if items_of_kind.nil?
30
30
 
31
- item = items_of_kind[key]
31
+ item = items_of_kind[key.to_sym]
32
32
  return nil if item.nil?
33
33
  return nil if item[:deleted]
34
34
 
@@ -10,12 +10,6 @@ module LaunchDarkly
10
10
  #
11
11
  # StatusProviderV2 is the FDv2-specific implementation of {LaunchDarkly::Interfaces::DataStore::StatusProvider}.
12
12
  #
13
- # This type is not stable, and not subject to any backwards
14
- # compatibility guarantees or semantic versioning. It is not suitable for production usage.
15
- #
16
- # Do not use it.
17
- # You have been warned.
18
- #
19
13
  class StatusProviderV2
20
14
  include LaunchDarkly::Interfaces::DataStore::StatusProvider
21
15
 
@@ -284,7 +284,7 @@ module LaunchDarkly
284
284
  # Convert a list of Changes to the pre-existing format used by FeatureStore.
285
285
  #
286
286
  # @param changes [Array<LaunchDarkly::Interfaces::DataSystem::Change>] List of changes
287
- # @return [Hash{DataKind => Hash{String => Hash}}] Hash suitable for FeatureStore operations
287
+ # @return [Hash{DataKind => Hash{Symbol => Hash}}] Hash suitable for FeatureStore operations
288
288
  #
289
289
  private def changes_to_store_data(changes)
290
290
  all_data = {
@@ -307,7 +307,7 @@ module LaunchDarkly
307
307
  #
308
308
  # Reset dependency tracker with new full data set.
309
309
  #
310
- # @param all_data [Hash{DataKind => Hash{String => Hash}}] Hash of data kinds to items
310
+ # @param all_data [Hash{DataKind => Hash{Symbol => Hash}}] Hash of data kinds to items
311
311
  # @return [void]
312
312
  #
313
313
  private def reset_dependency_tracker(all_data)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ldclient-rb/impl/data_system/http_config_options"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ module DataSystem
8
+ #
9
+ # DataSourceBuilderCommon is a mixin that provides common HTTP configuration
10
+ # setters for data source builders (polling and streaming).
11
+ #
12
+ # Each builder that includes this module must define a DEFAULT_BASE_URI constant.
13
+ #
14
+ module DataSourceBuilderCommon
15
+ #
16
+ # Sets the base URI for HTTP requests.
17
+ #
18
+ # @param uri [String]
19
+ # @return [self]
20
+ #
21
+ def base_uri(uri)
22
+ @base_uri = uri
23
+ self
24
+ end
25
+
26
+ #
27
+ # Sets a custom socket factory for HTTP connections.
28
+ #
29
+ # @param factory [Object]
30
+ # @return [self]
31
+ #
32
+ def socket_factory(factory)
33
+ @socket_factory = factory
34
+ self
35
+ end
36
+
37
+ #
38
+ # Sets the read timeout for HTTP connections.
39
+ #
40
+ # @param timeout [Float] Timeout in seconds
41
+ # @return [self]
42
+ #
43
+ def read_timeout(timeout)
44
+ @read_timeout = timeout
45
+ self
46
+ end
47
+
48
+ #
49
+ # Sets the connect timeout for HTTP connections.
50
+ #
51
+ # @param timeout [Float] Timeout in seconds
52
+ # @return [self]
53
+ #
54
+ def connect_timeout(timeout)
55
+ @connect_timeout = timeout
56
+ self
57
+ end
58
+
59
+ #
60
+ # Builds an HttpConfigOptions instance from the current builder settings.
61
+ # Uses self.class::DEFAULT_BASE_URI if base_uri was not explicitly set.
62
+ # Read/connect timeouts default to HttpConfigOptions defaults if not set.
63
+ #
64
+ # @return [HttpConfigOptions]
65
+ #
66
+ private def build_http_config
67
+ HttpConfigOptions.new(
68
+ base_uri: (@base_uri || self.class::DEFAULT_BASE_URI).chomp("/"),
69
+ socket_factory: @socket_factory,
70
+ read_timeout: @read_timeout,
71
+ connect_timeout: @connect_timeout
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end