loading
Generated 2020-08-24T22:21:54-04:00

All Files ( 38.02% covered at 28.31 hits/line )

254 files in total.
9554 relevant lines, 3632 lines covered and 5922 lines missed. ( 38.02% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/active_support.rb 92.19 % 108 64 59 5 22.11
lib/active_support/actionable_error.rb 64.71 % 48 17 11 6 0.71
lib/active_support/all.rb 0.00 % 5 3 0 3 0.00
lib/active_support/array_inquirer.rb 46.67 % 50 15 7 8 0.47
lib/active_support/backtrace_cleaner.rb 0.00 % 132 63 0 63 0.00
lib/active_support/benchmarkable.rb 38.46 % 51 13 5 8 0.77
lib/active_support/builder.rb 50.00 % 8 4 2 2 0.50
lib/active_support/cache.rb 28.38 % 855 296 84 212 3.69
lib/active_support/cache/file_store.rb 0.00 % 196 147 0 147 0.00
lib/active_support/cache/mem_cache_store.rb 0.00 % 205 130 0 130 0.00
lib/active_support/cache/memory_store.rb 0.00 % 171 127 0 127 0.00
lib/active_support/cache/null_store.rb 0.00 % 48 29 0 29 0.00
lib/active_support/cache/redis_cache_store.rb 30.24 % 494 205 62 143 0.91
lib/active_support/cache/strategy/local_cache.rb 37.14 % 197 105 39 66 1.49
lib/active_support/cache/strategy/local_cache_middleware.rb 0.00 % 45 36 0 36 0.00
lib/active_support/callbacks.rb 54.36 % 862 344 187 157 31.28
lib/active_support/concern.rb 82.22 % 215 45 37 8 40.00
lib/active_support/concurrency/load_interlock_aware_monitor.rb 52.94 % 35 17 9 8 0.53
lib/active_support/concurrency/share_lock.rb 27.84 % 226 97 27 70 0.84
lib/active_support/configurable.rb 75.00 % 146 32 24 8 1.31
lib/active_support/configuration_file.rb 0.00 % 46 33 0 33 0.00
lib/active_support/core_ext.rb 0.00 % 5 3 0 3 0.00
lib/active_support/core_ext/array.rb 100.00 % 9 7 7 0 1.00
lib/active_support/core_ext/array/access.rb 48.15 % 104 27 13 14 0.48
lib/active_support/core_ext/array/conversions.rb 23.91 % 213 46 11 35 5.50
lib/active_support/core_ext/array/extract.rb 28.57 % 21 7 2 5 0.29
lib/active_support/core_ext/array/extract_options.rb 100.00 % 31 8 8 0 65.38
lib/active_support/core_ext/array/grouping.rb 11.43 % 109 35 4 31 0.11
lib/active_support/core_ext/array/inquiry.rb 75.00 % 19 4 3 1 0.75
lib/active_support/core_ext/array/prepend_and_append.rb 0.00 % 5 2 0 2 0.00
lib/active_support/core_ext/array/wrap.rb 28.57 % 48 7 2 5 3.71
lib/active_support/core_ext/benchmark.rb 75.00 % 16 4 3 1 1.50
lib/active_support/core_ext/big_decimal.rb 100.00 % 3 1 1 0 1.00
lib/active_support/core_ext/big_decimal/conversions.rb 100.00 % 14 7 7 0 1.86
lib/active_support/core_ext/class.rb 100.00 % 4 2 2 0 1.00
lib/active_support/core_ext/class/attribute.rb 94.44 % 132 18 17 1 98.00
lib/active_support/core_ext/class/attribute_accessors.rb 0.00 % 6 1 0 1 0.00
lib/active_support/core_ext/class/subclasses.rb 50.00 % 33 6 3 3 0.50
lib/active_support/core_ext/date.rb 100.00 % 7 5 5 0 2.00
lib/active_support/core_ext/date/acts_like.rb 75.00 % 10 4 3 1 10.50
lib/active_support/core_ext/date/blank.rb 75.00 % 14 4 3 1 1.50
lib/active_support/core_ext/date/calculations.rb 60.00 % 146 70 42 28 13.80
lib/active_support/core_ext/date/conversions.rb 57.14 % 97 28 16 12 1.14
lib/active_support/core_ext/date/zones.rb 100.00 % 8 4 4 0 23.00
lib/active_support/core_ext/date_and_time/calculations.rb 51.88 % 364 133 69 64 11.93
lib/active_support/core_ext/date_and_time/compatibility.rb 100.00 % 31 5 5 0 24.00
lib/active_support/core_ext/date_and_time/zones.rb 38.46 % 40 13 5 8 8.85
lib/active_support/core_ext/date_time.rb 100.00 % 7 5 5 0 2.00
lib/active_support/core_ext/date_time/acts_like.rb 71.43 % 16 7 5 2 1.43
lib/active_support/core_ext/date_time/blank.rb 75.00 % 14 4 3 1 1.50
lib/active_support/core_ext/date_time/calculations.rb 48.81 % 211 84 41 43 11.23
lib/active_support/core_ext/date_time/compatibility.rb 85.71 % 18 7 6 1 1.71
lib/active_support/core_ext/date_time/conversions.rb 58.33 % 106 36 21 15 13.42
lib/active_support/core_ext/digest.rb 0.00 % 3 1 0 1 0.00
lib/active_support/core_ext/digest/uuid.rb 42.31 % 53 26 11 15 0.42
lib/active_support/core_ext/enumerable.rb 33.75 % 260 80 27 53 7.76
lib/active_support/core_ext/file.rb 100.00 % 3 1 1 0 1.00
lib/active_support/core_ext/file/atomic.rb 17.39 % 70 23 4 19 0.17
lib/active_support/core_ext/hash.rb 100.00 % 10 8 8 0 1.00
lib/active_support/core_ext/hash/compact.rb 0.00 % 5 2 0 2 0.00
lib/active_support/core_ext/hash/conversions.rb 36.78 % 263 87 32 55 0.37
lib/active_support/core_ext/hash/deep_merge.rb 30.00 % 34 10 3 7 7.20
lib/active_support/core_ext/hash/deep_transform_values.rb 37.50 % 46 16 6 10 0.38
lib/active_support/core_ext/hash/except.rb 50.00 % 24 6 3 3 12.00
lib/active_support/core_ext/hash/indifferent_access.rb 80.00 % 24 5 4 1 0.80
lib/active_support/core_ext/hash/keys.rb 39.53 % 143 43 17 26 9.09
lib/active_support/core_ext/hash/reverse_merge.rb 75.00 % 25 8 6 2 0.75
lib/active_support/core_ext/hash/slice.rb 30.00 % 26 10 3 7 7.20
lib/active_support/core_ext/hash/transform_values.rb 0.00 % 5 2 0 2 0.00
lib/active_support/core_ext/integer.rb 100.00 % 5 3 3 0 1.00
lib/active_support/core_ext/integer/inflections.rb 66.67 % 31 6 4 2 0.67
lib/active_support/core_ext/integer/multiple.rb 66.67 % 12 3 2 1 0.67
lib/active_support/core_ext/integer/time.rb 77.78 % 22 9 7 2 1.56
lib/active_support/core_ext/kernel.rb 100.00 % 5 3 3 0 1.00
lib/active_support/core_ext/kernel/concern.rb 80.00 % 14 5 4 1 0.80
lib/active_support/core_ext/kernel/reporting.rb 83.33 % 45 12 10 2 22.00
lib/active_support/core_ext/kernel/singleton_class.rb 66.67 % 8 3 2 1 0.67
lib/active_support/core_ext/load_error.rb 66.67 % 9 3 2 1 2.00
lib/active_support/core_ext/marshal.rb 50.00 % 26 12 6 6 4667.67
lib/active_support/core_ext/module.rb 100.00 % 13 11 11 0 1.00
lib/active_support/core_ext/module/aliasing.rb 100.00 % 31 3 3 0 2.67
lib/active_support/core_ext/module/anonymous.rb 66.67 % 30 3 2 1 2.00
lib/active_support/core_ext/module/attr_internal.rb 50.00 % 38 20 10 10 0.55
lib/active_support/core_ext/module/attribute_accessors.rb 100.00 % 206 32 32 0 179.63
lib/active_support/core_ext/module/attribute_accessors_per_thread.rb 100.00 % 148 21 21 0 2.43
lib/active_support/core_ext/module/concerning.rb 100.00 % 140 11 11 0 1.45
lib/active_support/core_ext/module/delegation.rb 92.86 % 330 42 39 3 349.93
lib/active_support/core_ext/module/deprecation.rb 100.00 % 25 3 3 0 16.67
lib/active_support/core_ext/module/introspection.rb 31.03 % 87 29 9 20 0.93
lib/active_support/core_ext/module/reachable.rb 0.00 % 6 3 0 3 0.00
lib/active_support/core_ext/module/redefine_method.rb 87.50 % 40 16 14 2 273.06
lib/active_support/core_ext/module/remove_method.rb 57.14 % 17 7 4 3 0.57
lib/active_support/core_ext/name_error.rb 32.00 % 65 25 8 17 0.96
lib/active_support/core_ext/numeric.rb 100.00 % 5 3 3 0 1.00
lib/active_support/core_ext/numeric/bytes.rb 78.57 % 66 28 22 6 10.46
lib/active_support/core_ext/numeric/conversions.rb 52.38 % 141 21 11 10 447.95
lib/active_support/core_ext/numeric/inquiry.rb 0.00 % 5 2 0 2 0.00
lib/active_support/core_ext/numeric/time.rb 73.08 % 66 26 19 7 10.23
lib/active_support/core_ext/object.rb 100.00 % 16 12 12 0 1.00
lib/active_support/core_ext/object/acts_like.rb 28.57 % 21 7 2 5 6.57
lib/active_support/core_ext/object/blank.rb 71.43 % 155 35 25 10 23.83
lib/active_support/core_ext/object/conversions.rb 100.00 % 6 4 4 0 1.00
lib/active_support/core_ext/object/deep_dup.rb 43.75 % 55 16 7 9 0.44
lib/active_support/core_ext/object/duplicable.rb 66.67 % 49 9 6 3 0.67
lib/active_support/core_ext/object/inclusion.rb 50.00 % 29 6 3 3 1.00
lib/active_support/core_ext/object/instance_variables.rb 60.00 % 30 5 3 2 1.20
lib/active_support/core_ext/object/json.rb 58.88 % 232 107 63 44 1.35
lib/active_support/core_ext/object/to_param.rb 100.00 % 3 1 1 0 23.00
lib/active_support/core_ext/object/to_query.rb 51.61 % 89 31 16 15 11.87
lib/active_support/core_ext/object/try.rb 56.00 % 158 25 14 11 13.44
lib/active_support/core_ext/object/with_options.rb 60.00 % 82 5 3 2 1.20
lib/active_support/core_ext/range.rb 100.00 % 7 5 5 0 1.00
lib/active_support/core_ext/range/compare_range.rb 22.22 % 82 27 6 21 0.22
lib/active_support/core_ext/range/conversions.rb 53.85 % 41 13 7 6 0.54
lib/active_support/core_ext/range/each.rb 84.62 % 24 13 11 2 1.77
lib/active_support/core_ext/range/include_range.rb 0.00 % 9 5 0 5 0.00
lib/active_support/core_ext/range/include_time_with_zone.rb 60.00 % 28 10 6 4 0.60
lib/active_support/core_ext/range/overlaps.rb 66.67 % 10 3 2 1 0.67
lib/active_support/core_ext/regexp.rb 66.67 % 7 3 2 1 0.67
lib/active_support/core_ext/securerandom.rb 42.86 % 45 14 6 8 0.43
lib/active_support/core_ext/string.rb 100.00 % 15 13 13 0 1.00
lib/active_support/core_ext/string/access.rb 50.00 % 95 12 6 6 0.50
lib/active_support/core_ext/string/behavior.rb 66.67 % 8 3 2 1 0.67
lib/active_support/core_ext/string/conversions.rb 40.00 % 59 15 6 9 0.80
lib/active_support/core_ext/string/exclude.rb 66.67 % 13 3 2 1 0.67
lib/active_support/core_ext/string/filters.rb 19.51 % 145 41 8 33 4.49
lib/active_support/core_ext/string/indent.rb 42.86 % 45 7 3 4 0.43
lib/active_support/core_ext/string/inflections.rb 55.81 % 293 43 24 19 28.35
lib/active_support/core_ext/string/inquiry.rb 80.00 % 16 5 4 1 0.80
lib/active_support/core_ext/string/multibyte.rb 44.44 % 58 9 4 5 10.22
lib/active_support/core_ext/string/output_safety.rb 58.72 % 314 109 64 45 0.95
lib/active_support/core_ext/string/starts_ends_with.rb 100.00 % 6 3 3 0 1.00
lib/active_support/core_ext/string/strip.rb 50.00 % 27 4 2 2 0.50
lib/active_support/core_ext/string/zones.rb 57.14 % 16 7 4 3 1.14
lib/active_support/core_ext/symbol.rb 100.00 % 3 1 1 0 1.00
lib/active_support/core_ext/symbol/starts_ends_with.rb 85.71 % 14 7 6 1 23.14
lib/active_support/core_ext/time.rb 100.00 % 7 5 5 0 2.00
lib/active_support/core_ext/time/acts_like.rb 75.00 % 10 4 3 1 17.25
lib/active_support/core_ext/time/calculations.rb 46.26 % 344 147 68 79 24.06
lib/active_support/core_ext/time/compatibility.rb 85.71 % 16 7 6 1 1.71
lib/active_support/core_ext/time/conversions.rb 50.00 % 73 18 9 9 11.50
lib/active_support/core_ext/time/zones.rb 42.86 % 113 28 12 16 9.86
lib/active_support/core_ext/uri.rb 64.29 % 29 14 9 5 0.64
lib/active_support/current_attributes.rb 70.69 % 208 58 41 17 1.22
lib/active_support/current_attributes/test_helper.rb 42.86 % 13 7 3 4 0.43
lib/active_support/dependencies.rb 38.07 % 822 352 134 218 16.36
lib/active_support/dependencies/autoload.rb 75.00 % 79 32 24 8 190.31
lib/active_support/dependencies/interlock.rb 53.57 % 57 28 15 13 1.61
lib/active_support/dependencies/zeitwerk_integration.rb 42.86 % 117 63 27 36 0.43
lib/active_support/deprecation.rb 100.00 % 51 26 26 0 23.35
lib/active_support/deprecation/behaviors.rb 36.11 % 122 36 13 23 8.67
lib/active_support/deprecation/constant_accessor.rb 68.75 % 52 16 11 5 6.56
lib/active_support/deprecation/disallowed.rb 33.33 % 56 24 8 16 8.00
lib/active_support/deprecation/instance_delegator.rb 80.95 % 38 21 17 4 65.14
lib/active_support/deprecation/method_wrappers.rb 84.62 % 85 26 22 4 8.38
lib/active_support/deprecation/proxy_wrappers.rb 60.66 % 177 61 37 24 84.39
lib/active_support/deprecation/reporting.rb 32.76 % 157 58 19 39 7.86
lib/active_support/descendants_tracker.rb 58.62 % 112 58 34 24 48.43
lib/active_support/digest.rb 0.00 % 20 16 0 16 0.00
lib/active_support/duration.rb 37.62 % 441 210 79 131 8.65
lib/active_support/duration/iso8601_parser.rb 0.00 % 123 90 0 90 0.00
lib/active_support/duration/iso8601_serializer.rb 0.00 % 65 47 0 47 0.00
lib/active_support/encrypted_configuration.rb 66.67 % 45 24 16 8 1.33
lib/active_support/encrypted_file.rb 49.02 % 101 51 25 26 0.98
lib/active_support/environment_inquirer.rb 70.00 % 20 10 7 3 0.90
lib/active_support/evented_file_update_checker.rb 0.00 % 216 143 0 143 0.00
lib/active_support/execution_wrapper.rb 0.00 % 129 90 0 90 0.00
lib/active_support/executor.rb 0.00 % 8 5 0 5 0.00
lib/active_support/file_update_checker.rb 0.00 % 162 89 0 89 0.00
lib/active_support/fork_tracker.rb 0.00 % 58 49 0 49 0.00
lib/active_support/gem_version.rb 88.89 % 17 9 8 1 21.33
lib/active_support/gzip.rb 0.00 % 38 23 0 23 0.00
lib/active_support/hash_with_indifferent_access.rb 45.26 % 412 137 62 75 0.45
lib/active_support/i18n.rb 81.82 % 16 11 9 2 19.64
lib/active_support/i18n_railtie.rb 0.00 % 138 103 0 103 0.00
lib/active_support/inflections.rb 100.00 % 72 58 58 0 24.00
lib/active_support/inflector.rb 100.00 % 9 5 5 0 23.00
lib/active_support/inflector/inflections.rb 77.65 % 255 85 66 19 230.21
lib/active_support/inflector/methods.rb 40.21 % 401 97 39 58 148.51
lib/active_support/inflector/transliterate.rb 24.14 % 147 29 7 22 5.55
lib/active_support/json.rb 100.00 % 4 2 2 0 2.00
lib/active_support/json/decoding.rb 41.94 % 75 31 13 18 0.84
lib/active_support/json/encoding.rb 59.62 % 138 52 31 21 1.19
lib/active_support/key_generator.rb 0.00 % 41 22 0 22 0.00
lib/active_support/lazy_load_hooks.rb 53.57 % 81 28 15 13 17.86
lib/active_support/locale/en.rb 0.00 % 33 30 0 30 0.00
lib/active_support/log_subscriber.rb 68.29 % 137 41 28 13 0.80
lib/active_support/log_subscriber/test_helper.rb 52.63 % 106 38 20 18 0.66
lib/active_support/logger.rb 22.92 % 93 48 11 37 5.50
lib/active_support/logger_silence.rb 81.25 % 34 16 13 3 19.75
lib/active_support/logger_thread_safe_level.rb 46.15 % 85 39 18 21 14.21
lib/active_support/message_encryptor.rb 40.00 % 224 75 30 45 0.80
lib/active_support/message_verifier.rb 45.00 % 205 40 18 22 0.90
lib/active_support/messages/metadata.rb 47.37 % 72 38 18 20 0.95
lib/active_support/messages/rotation_configuration.rb 54.55 % 23 11 6 5 1.09
lib/active_support/messages/rotator.rb 54.84 % 57 31 17 14 1.10
lib/active_support/multibyte.rb 75.00 % 23 8 6 2 17.25
lib/active_support/multibyte/chars.rb 0.00 % 216 98 0 98 0.00
lib/active_support/multibyte/unicode.rb 37.74 % 156 53 20 33 0.83
lib/active_support/notifications.rb 62.50 % 280 32 20 12 14.31
lib/active_support/notifications/fanout.rb 53.90 % 259 141 76 65 10.45
lib/active_support/notifications/instrumenter.rb 43.04 % 165 79 34 45 10.33
lib/active_support/number_helper.rb 75.00 % 397 28 21 7 0.75
lib/active_support/number_helper/number_converter.rb 0.00 % 183 125 0 125 0.00
lib/active_support/number_helper/number_to_currency_converter.rb 0.00 % 45 34 0 34 0.00
lib/active_support/number_helper/number_to_delimited_converter.rb 0.00 % 30 23 0 23 0.00
lib/active_support/number_helper/number_to_human_converter.rb 0.00 % 69 56 0 56 0.00
lib/active_support/number_helper/number_to_human_size_converter.rb 0.00 % 60 46 0 46 0.00
lib/active_support/number_helper/number_to_percentage_converter.rb 0.00 % 16 12 0 12 0.00
lib/active_support/number_helper/number_to_phone_converter.rb 0.00 % 59 48 0 48 0.00
lib/active_support/number_helper/number_to_rounded_converter.rb 0.00 % 55 46 0 46 0.00
lib/active_support/number_helper/rounding_helper.rb 0.00 % 50 41 0 41 0.00
lib/active_support/option_merger.rb 42.31 % 46 26 11 15 6.62
lib/active_support/ordered_hash.rb 58.82 % 50 17 10 7 0.59
lib/active_support/ordered_options.rb 43.33 % 91 30 13 17 0.87
lib/active_support/parameter_filter.rb 25.00 % 133 60 15 45 0.25
lib/active_support/per_thread_registry.rb 72.73 % 60 11 8 3 17.91
lib/active_support/proxy_object.rb 0.00 % 15 9 0 9 0.00
lib/active_support/rails.rb 0.00 % 26 5 0 5 0.00
lib/active_support/railtie.rb 0.00 % 93 73 0 73 0.00
lib/active_support/reloader.rb 0.00 % 130 81 0 81 0.00
lib/active_support/rescuable.rb 46.43 % 174 56 26 30 0.96
lib/active_support/secure_compare_rotator.rb 73.33 % 51 15 11 4 0.73
lib/active_support/security_utils.rb 50.00 % 31 12 6 6 1.00
lib/active_support/string_inquirer.rb 60.00 % 35 10 6 4 0.60
lib/active_support/subscriber.rb 72.60 % 169 73 53 20 1.48
lib/active_support/tagged_logging.rb 39.62 % 113 53 21 32 0.40
lib/active_support/test_case.rb 88.52 % 163 61 54 7 34.70
lib/active_support/testing/assertions.rb 18.64 % 235 59 11 48 4.29
lib/active_support/testing/autorun.rb 100.00 % 7 3 3 0 24.00
lib/active_support/testing/constant_lookup.rb 53.33 % 51 15 8 7 12.27
lib/active_support/testing/declarative.rb 83.33 % 28 12 10 2 228.75
lib/active_support/testing/deprecation.rb 29.17 % 38 24 7 17 6.71
lib/active_support/testing/file_fixtures.rb 61.54 % 38 13 8 5 14.15
lib/active_support/testing/isolation.rb 26.67 % 110 60 16 44 5.50
lib/active_support/testing/method_call_assertions.rb 28.95 % 70 38 11 27 6.95
lib/active_support/testing/parallelization.rb 93.10 % 51 29 27 2 223.14
lib/active_support/testing/parallelization/server.rb 85.71 % 78 42 36 6 1283.90
lib/active_support/testing/parallelization/worker.rb 35.19 % 100 54 19 35 10.22
lib/active_support/testing/setup_and_teardown.rb 71.43 % 55 21 15 6 16.10
lib/active_support/testing/stream.rb 25.93 % 43 27 7 20 0.26
lib/active_support/testing/tagged_logging.rb 46.67 % 27 15 7 8 10.73
lib/active_support/testing/time_helpers.rb 37.10 % 222 62 23 39 8.53
lib/active_support/time.rb 100.00 % 20 13 13 0 2.00
lib/active_support/time_with_zone.rb 46.39 % 585 194 90 104 12.09
lib/active_support/values/time_zone.rb 50.36 % 580 137 69 68 62.54
lib/active_support/version.rb 75.00 % 10 4 3 1 18.00
lib/active_support/xml_mini.rb 41.12 % 201 107 44 63 9.46
lib/active_support/xml_mini/jdom.rb 0.00 % 182 116 0 116 0.00
lib/active_support/xml_mini/libxml.rb 0.00 % 80 53 0 53 0.00
lib/active_support/xml_mini/libxmlsax.rb 0.00 % 83 62 0 62 0.00
lib/active_support/xml_mini/nokogiri.rb 0.00 % 83 58 0 58 0.00
lib/active_support/xml_mini/nokogirisax.rb 0.00 % 86 66 0 66 0.00
lib/active_support/xml_mini/rexml.rb 30.61 % 130 49 15 34 7.04

lib/active_support.rb

92.19% lines covered

64 relevant lines. 59 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2005-2020 David Heinemeier Hansson
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #++
  24. 24 require "securerandom"
  25. 24 require "active_support/dependencies/autoload"
  26. 24 require "active_support/version"
  27. 24 require "active_support/logger"
  28. 24 require "active_support/lazy_load_hooks"
  29. 24 require "active_support/core_ext/date_and_time/compatibility"
  30. 24 module ActiveSupport
  31. 24 extend ActiveSupport::Autoload
  32. 24 autoload :Concern
  33. 24 autoload :ActionableError
  34. 24 autoload :ConfigurationFile
  35. 24 autoload :CurrentAttributes
  36. 24 autoload :Dependencies
  37. 24 autoload :DescendantsTracker
  38. 24 autoload :ExecutionWrapper
  39. 24 autoload :Executor
  40. 24 autoload :FileUpdateChecker
  41. 24 autoload :EventedFileUpdateChecker
  42. 24 autoload :ForkTracker
  43. 24 autoload :LogSubscriber
  44. 24 autoload :Notifications
  45. 24 autoload :Reloader
  46. 24 autoload :SecureCompareRotator
  47. 24 eager_autoload do
  48. 24 autoload :BacktraceCleaner
  49. 24 autoload :ProxyObject
  50. 24 autoload :Benchmarkable
  51. 24 autoload :Cache
  52. 24 autoload :Callbacks
  53. 24 autoload :Configurable
  54. 24 autoload :Deprecation
  55. 24 autoload :Digest
  56. 24 autoload :Gzip
  57. 24 autoload :Inflector
  58. 24 autoload :JSON
  59. 24 autoload :KeyGenerator
  60. 24 autoload :MessageEncryptor
  61. 24 autoload :MessageVerifier
  62. 24 autoload :Multibyte
  63. 24 autoload :NumberHelper
  64. 24 autoload :OptionMerger
  65. 24 autoload :OrderedHash
  66. 24 autoload :OrderedOptions
  67. 24 autoload :StringInquirer
  68. 24 autoload :EnvironmentInquirer
  69. 24 autoload :TaggedLogging
  70. 24 autoload :XmlMini
  71. 24 autoload :ArrayInquirer
  72. end
  73. 24 autoload :Rescuable
  74. 24 autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
  75. 24 autoload :TestCase
  76. 24 def self.eager_load!
  77. super
  78. NumberHelper.eager_load!
  79. end
  80. 24 cattr_accessor :test_order # :nodoc:
  81. 24 def self.to_time_preserves_timezone
  82. DateAndTime::Compatibility.preserve_timezone
  83. end
  84. 24 def self.to_time_preserves_timezone=(value)
  85. 23 DateAndTime::Compatibility.preserve_timezone = value
  86. end
  87. 24 def self.utc_to_local_returns_utc_offset_times
  88. DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times
  89. end
  90. 24 def self.utc_to_local_returns_utc_offset_times=(value)
  91. DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
  92. end
  93. end
  94. 24 autoload :I18n, "active_support/i18n"

lib/active_support/actionable_error.rb

64.71% lines covered

17 relevant lines. 11 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport
  3. # Actionable errors let's you define actions to resolve an error.
  4. #
  5. # To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt>
  6. # module and invoke the +action+ class macro to define the action. An action
  7. # needs a name and a block to execute.
  8. 1 module ActionableError
  9. 1 extend Concern
  10. 1 class NonActionable < StandardError; end
  11. 1 included do
  12. 1 class_attribute :_actions, default: {}
  13. end
  14. 1 def self.actions(error) # :nodoc:
  15. case error
  16. when ActionableError, -> it { Class === it && it < ActionableError }
  17. error._actions
  18. else
  19. {}
  20. end
  21. end
  22. 1 def self.dispatch(error, name) # :nodoc:
  23. actions(error).fetch(name).call
  24. rescue KeyError
  25. raise NonActionable, "Cannot find action \"#{name}\""
  26. end
  27. 1 module ClassMethods
  28. # Defines an action that can resolve the error.
  29. #
  30. # class PendingMigrationError < MigrationError
  31. # include ActiveSupport::ActionableError
  32. #
  33. # action "Run pending migrations" do
  34. # ActiveRecord::Tasks::DatabaseTasks.migrate
  35. # end
  36. # end
  37. 1 def action(name, &block)
  38. 2 _actions[name] = block
  39. end
  40. end
  41. end
  42. end

lib/active_support/all.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support"
  3. require "active_support/time"
  4. require "active_support/core_ext"

lib/active_support/array_inquirer.rb

46.67% lines covered

15 relevant lines. 7 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/symbol/starts_ends_with"
  3. 1 module ActiveSupport
  4. # Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check
  5. # its string-like contents:
  6. #
  7. # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
  8. #
  9. # variants.phone? # => true
  10. # variants.tablet? # => true
  11. # variants.desktop? # => false
  12. 1 class ArrayInquirer < Array
  13. # Passes each element of +candidates+ collection to ArrayInquirer collection.
  14. # The method returns true if any element from the ArrayInquirer collection
  15. # is equal to the stringified or symbolized form of any element in the +candidates+ collection.
  16. #
  17. # If +candidates+ collection is not given, method returns true.
  18. #
  19. # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
  20. #
  21. # variants.any? # => true
  22. # variants.any?(:phone, :tablet) # => true
  23. # variants.any?('phone', 'desktop') # => true
  24. # variants.any?(:desktop, :watch) # => false
  25. 1 def any?(*candidates)
  26. if candidates.none?
  27. super
  28. else
  29. candidates.any? do |candidate|
  30. include?(candidate.to_sym) || include?(candidate.to_s)
  31. end
  32. end
  33. end
  34. 1 private
  35. 1 def respond_to_missing?(name, include_private = false)
  36. name.end_with?("?") || super
  37. end
  38. 1 def method_missing(name, *args)
  39. if name.end_with?("?")
  40. any?(name[0..-2])
  41. else
  42. super
  43. end
  44. end
  45. end
  46. end

lib/active_support/backtrace_cleaner.rb

0.0% lines covered

63 relevant lines. 0 lines covered and 63 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. # Backtraces often include many lines that are not relevant for the context
  4. # under review. This makes it hard to find the signal amongst the backtrace
  5. # noise, and adds debugging time. With a BacktraceCleaner, filters and
  6. # silencers are used to remove the noisy lines, so that only the most relevant
  7. # lines remain.
  8. #
  9. # Filters are used to modify lines of data, while silencers are used to remove
  10. # lines entirely. The typical filter use case is to remove lengthy path
  11. # information from the start of each line, and view file paths relevant to the
  12. # app directory instead of the file system root. The typical silencer use case
  13. # is to exclude the output of a noisy library from the backtrace, so that you
  14. # can focus on the rest.
  15. #
  16. # bc = ActiveSupport::BacktraceCleaner.new
  17. # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
  18. # bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
  19. # bc.clean(exception.backtrace) # perform the cleanup
  20. #
  21. # To reconfigure an existing BacktraceCleaner (like the default one in Rails)
  22. # and show as much data as possible, you can always call
  23. # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
  24. # backtrace to a pristine state. If you need to reconfigure an existing
  25. # BacktraceCleaner so that it does not filter or modify the paths of any lines
  26. # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
  27. # These two methods will give you a completely untouched backtrace.
  28. #
  29. # Inspired by the Quiet Backtrace gem by thoughtbot.
  30. class BacktraceCleaner
  31. def initialize
  32. @filters, @silencers = [], []
  33. add_gem_filter
  34. add_gem_silencer
  35. add_stdlib_silencer
  36. end
  37. # Returns the backtrace after all filters and silencers have been run
  38. # against it. Filters run first, then silencers.
  39. def clean(backtrace, kind = :silent)
  40. filtered = filter_backtrace(backtrace)
  41. case kind
  42. when :silent
  43. silence(filtered)
  44. when :noise
  45. noise(filtered)
  46. else
  47. filtered
  48. end
  49. end
  50. alias :filter :clean
  51. # Adds a filter from the block provided. Each line in the backtrace will be
  52. # mapped against this filter.
  53. #
  54. # # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
  55. # backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
  56. def add_filter(&block)
  57. @filters << block
  58. end
  59. # Adds a silencer from the block provided. If the silencer returns +true+
  60. # for a given line, it will be excluded from the clean backtrace.
  61. #
  62. # # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
  63. # backtrace_cleaner.add_silencer { |line| /puma/.match?(line) }
  64. def add_silencer(&block)
  65. @silencers << block
  66. end
  67. # Removes all silencers, but leaves in the filters. Useful if your
  68. # context of debugging suddenly expands as you suspect a bug in one of
  69. # the libraries you use.
  70. def remove_silencers!
  71. @silencers = []
  72. end
  73. # Removes all filters, but leaves in the silencers. Useful if you suddenly
  74. # need to see entire filepaths in the backtrace that you had already
  75. # filtered out.
  76. def remove_filters!
  77. @filters = []
  78. end
  79. private
  80. FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) /
  81. def add_gem_filter
  82. gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
  83. return if gems_paths.empty?
  84. gems_regexp = %r{(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)}
  85. gems_result = '\3 (\4) \5'
  86. add_filter { |line| line.sub(gems_regexp, gems_result) }
  87. end
  88. def add_gem_silencer
  89. add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) }
  90. end
  91. def add_stdlib_silencer
  92. add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) }
  93. end
  94. # Process +ary+ via +filters+ using +method+, ensuring
  95. # _something_ gets returned.
  96. def process_collection(ary, filters, method)
  97. filters.reduce(ary) { |bt, f| bt.send(method) { |line| f.call(line) } }
  98. end
  99. # Use @filters to transform the backtrace via map
  100. def filter_backtrace(backtrace)
  101. process_collection backtrace, @filters, :map
  102. end
  103. # Use @silencers to reject parts of the backtrace. Guarantee
  104. # something non-empty is returned.
  105. def silence(backtrace)
  106. result = process_collection backtrace, @silencers, :reject
  107. result.first ? result : backtrace.dup
  108. end
  109. # Use @silencers to select parts of the backtrace. Guarantee
  110. # something non-empty is returned.
  111. def noise(backtrace)
  112. result = backtrace.select { |line| @silencers.any? { |s| s.call(line) } }
  113. result.first ? result : backtrace.dup
  114. end
  115. end
  116. end

lib/active_support/benchmarkable.rb

38.46% lines covered

13 relevant lines. 5 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/benchmark"
  3. 2 require "active_support/core_ext/hash/keys"
  4. 2 module ActiveSupport
  5. 2 module Benchmarkable
  6. # Allows you to measure the execution time of a block in a template and
  7. # records the result to the log. Wrap this block around expensive operations
  8. # or possible bottlenecks to get a time reading for the operation. For
  9. # example, let's say you thought your file processing method was taking too
  10. # long; you could wrap it in a benchmark block.
  11. #
  12. # <% benchmark 'Process data files' do %>
  13. # <%= expensive_files_operation %>
  14. # <% end %>
  15. #
  16. # That would add something like "Process data files (345.2ms)" to the log,
  17. # which you can then use to compare timings when optimizing your code.
  18. #
  19. # You may give an optional logger level (<tt>:debug</tt>, <tt>:info</tt>,
  20. # <tt>:warn</tt>, <tt>:error</tt>) as the <tt>:level</tt> option. The
  21. # default logger level value is <tt>:info</tt>.
  22. #
  23. # <% benchmark 'Low-level files', level: :debug do %>
  24. # <%= lowlevel_files_operation %>
  25. # <% end %>
  26. #
  27. # Finally, you can pass true as the third argument to silence all log
  28. # activity (other than the timing information) from inside the block. This
  29. # is great for boiling down a noisy block to just a single statement that
  30. # produces one log line:
  31. #
  32. # <% benchmark 'Process data files', level: :info, silence: true do %>
  33. # <%= expensive_and_chatty_files_operation %>
  34. # <% end %>
  35. 2 def benchmark(message = "Benchmarking", options = {})
  36. if logger
  37. options.assert_valid_keys(:level, :silence)
  38. options[:level] ||= :info
  39. result = nil
  40. ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }
  41. logger.send(options[:level], "%s (%.1fms)" % [ message, ms ])
  42. result
  43. else
  44. yield
  45. end
  46. end
  47. end
  48. end

lib/active_support/builder.rb

50.0% lines covered

4 relevant lines. 2 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 begin
  3. 1 require "builder"
  4. rescue LoadError => e
  5. $stderr.puts "You don't have builder installed in your application. Please add it to your Gemfile and run bundle install"
  6. raise e
  7. end

lib/active_support/cache.rb

28.38% lines covered

296 relevant lines. 84 lines covered and 212 lines missed.
    
  1. # frozen_string_literal: true
  2. 13 require "zlib"
  3. 13 require "active_support/core_ext/array/extract_options"
  4. 13 require "active_support/core_ext/array/wrap"
  5. 13 require "active_support/core_ext/enumerable"
  6. 13 require "active_support/core_ext/module/attribute_accessors"
  7. 13 require "active_support/core_ext/numeric/bytes"
  8. 13 require "active_support/core_ext/numeric/time"
  9. 13 require "active_support/core_ext/object/to_param"
  10. 13 require "active_support/core_ext/object/try"
  11. 13 require "active_support/core_ext/string/inflections"
  12. 13 module ActiveSupport
  13. # See ActiveSupport::Cache::Store for documentation.
  14. 13 module Cache
  15. 13 autoload :FileStore, "active_support/cache/file_store"
  16. 13 autoload :MemoryStore, "active_support/cache/memory_store"
  17. 13 autoload :MemCacheStore, "active_support/cache/mem_cache_store"
  18. 13 autoload :NullStore, "active_support/cache/null_store"
  19. 13 autoload :RedisCacheStore, "active_support/cache/redis_cache_store"
  20. # These options mean something to all cache implementations. Individual cache
  21. # implementations may support additional options.
  22. 13 UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
  23. 13 module Strategy
  24. 13 autoload :LocalCache, "active_support/cache/strategy/local_cache"
  25. end
  26. 13 class << self
  27. # Creates a new Store object according to the given options.
  28. #
  29. # If no arguments are passed to this method, then a new
  30. # ActiveSupport::Cache::MemoryStore object will be returned.
  31. #
  32. # If you pass a Symbol as the first argument, then a corresponding cache
  33. # store class under the ActiveSupport::Cache namespace will be created.
  34. # For example:
  35. #
  36. # ActiveSupport::Cache.lookup_store(:memory_store)
  37. # # => returns a new ActiveSupport::Cache::MemoryStore object
  38. #
  39. # ActiveSupport::Cache.lookup_store(:mem_cache_store)
  40. # # => returns a new ActiveSupport::Cache::MemCacheStore object
  41. #
  42. # Any additional arguments will be passed to the corresponding cache store
  43. # class's constructor:
  44. #
  45. # ActiveSupport::Cache.lookup_store(:file_store, '/tmp/cache')
  46. # # => same as: ActiveSupport::Cache::FileStore.new('/tmp/cache')
  47. #
  48. # If the first argument is not a Symbol, then it will simply be returned:
  49. #
  50. # ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
  51. # # => returns MyOwnCacheStore.new
  52. 13 def lookup_store(store = nil, *parameters)
  53. case store
  54. when Symbol
  55. options = parameters.extract_options!
  56. retrieve_store_class(store).new(*parameters, **options)
  57. when Array
  58. lookup_store(*store)
  59. when nil
  60. ActiveSupport::Cache::MemoryStore.new
  61. else
  62. store
  63. end
  64. end
  65. # Expands out the +key+ argument into a key that can be used for the
  66. # cache store. Optionally accepts a namespace, and all keys will be
  67. # scoped within that namespace.
  68. #
  69. # If the +key+ argument provided is an array, or responds to +to_a+, then
  70. # each of elements in the array will be turned into parameters/keys and
  71. # concatenated into a single key. For example:
  72. #
  73. # ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar"
  74. # ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar"
  75. #
  76. # The +key+ argument can also respond to +cache_key+ or +to_param+.
  77. 13 def expand_cache_key(key, namespace = nil)
  78. expanded_cache_key = namespace ? +"#{namespace}/" : +""
  79. if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
  80. expanded_cache_key << "#{prefix}/"
  81. end
  82. expanded_cache_key << retrieve_cache_key(key)
  83. expanded_cache_key
  84. end
  85. 13 private
  86. 13 def retrieve_cache_key(key)
  87. case
  88. when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version
  89. when key.respond_to?(:cache_key) then key.cache_key
  90. when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
  91. when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
  92. else key.to_param
  93. end.to_s
  94. end
  95. # Obtains the specified cache store class, given the name of the +store+.
  96. # Raises an error when the store class cannot be found.
  97. 13 def retrieve_store_class(store)
  98. # require_relative cannot be used here because the class might be
  99. # provided by another gem, like redis-activesupport for example.
  100. require "active_support/cache/#{store}"
  101. rescue LoadError => e
  102. raise "Could not find cache store adapter for #{store} (#{e})"
  103. else
  104. ActiveSupport::Cache.const_get(store.to_s.camelize)
  105. end
  106. end
  107. # An abstract cache store class. There are multiple cache store
  108. # implementations, each having its own additional features. See the classes
  109. # under the ActiveSupport::Cache module, e.g.
  110. # ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
  111. # popular cache store for large production websites.
  112. #
  113. # Some implementations may not support all methods beyond the basic cache
  114. # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
  115. #
  116. # ActiveSupport::Cache::Store can store any serializable Ruby object.
  117. #
  118. # cache = ActiveSupport::Cache::MemoryStore.new
  119. #
  120. # cache.read('city') # => nil
  121. # cache.write('city', "Duckburgh")
  122. # cache.read('city') # => "Duckburgh"
  123. #
  124. # Keys are always translated into Strings and are case sensitive. When an
  125. # object is specified as a key and has a +cache_key+ method defined, this
  126. # method will be called to define the key. Otherwise, the +to_param+
  127. # method will be called. Hashes and Arrays can also be used as keys. The
  128. # elements will be delimited by slashes, and the elements within a Hash
  129. # will be sorted by key so they are consistent.
  130. #
  131. # cache.read('city') == cache.read(:city) # => true
  132. #
  133. # Nil values can be cached.
  134. #
  135. # If your cache is on a shared infrastructure, you can define a namespace
  136. # for your cache entries. If a namespace is defined, it will be prefixed on
  137. # to every key. The namespace can be either a static value or a Proc. If it
  138. # is a Proc, it will be invoked when each key is evaluated so that you can
  139. # use application logic to invalidate keys.
  140. #
  141. # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
  142. # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
  143. #
  144. # Cached data larger than 1kB are compressed by default. To turn off
  145. # compression, pass <tt>compress: false</tt> to the initializer or to
  146. # individual +fetch+ or +write+ method calls. The 1kB compression
  147. # threshold is configurable with the <tt>:compress_threshold</tt> option,
  148. # specified in bytes.
  149. 13 class Store
  150. 13 cattr_accessor :logger, instance_writer: true
  151. 13 attr_reader :silence, :options
  152. 13 alias :silence? :silence
  153. 13 class << self
  154. 13 private
  155. 13 def retrieve_pool_options(options)
  156. {}.tap do |pool_options|
  157. pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
  158. pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
  159. end
  160. end
  161. 13 def ensure_connection_pool_added!
  162. require "connection_pool"
  163. rescue LoadError => e
  164. $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
  165. raise e
  166. end
  167. end
  168. # Creates a new cache. The options will be passed to any write method calls
  169. # except for <tt>:namespace</tt> which can be used to set the global
  170. # namespace for the cache.
  171. 13 def initialize(options = nil)
  172. @options = options ? options.dup : {}
  173. end
  174. # Silences the logger.
  175. 13 def silence!
  176. @silence = true
  177. self
  178. end
  179. # Silences the logger within a block.
  180. 13 def mute
  181. previous_silence, @silence = defined?(@silence) && @silence, true
  182. yield
  183. ensure
  184. @silence = previous_silence
  185. end
  186. # Fetches data from the cache, using the given key. If there is data in
  187. # the cache with the given key, then that data is returned.
  188. #
  189. # If there is no such data in the cache (a cache miss), then +nil+ will be
  190. # returned. However, if a block has been passed, that block will be passed
  191. # the key and executed in the event of a cache miss. The return value of the
  192. # block will be written to the cache under the given cache key, and that
  193. # return value will be returned.
  194. #
  195. # cache.write('today', 'Monday')
  196. # cache.fetch('today') # => "Monday"
  197. #
  198. # cache.fetch('city') # => nil
  199. # cache.fetch('city') do
  200. # 'Duckburgh'
  201. # end
  202. # cache.fetch('city') # => "Duckburgh"
  203. #
  204. # You may also specify additional options via the +options+ argument.
  205. # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
  206. # the cache value as missing even if it's present. Passing a block is
  207. # required when +force+ is true so this always results in a cache write.
  208. #
  209. # cache.write('today', 'Monday')
  210. # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
  211. # cache.fetch('today', force: true) # => ArgumentError
  212. #
  213. # The +:force+ option is useful when you're calling some other method to
  214. # ask whether you should force a cache write. Otherwise, it's clearer to
  215. # just call <tt>Cache#write</tt>.
  216. #
  217. # Setting <tt>skip_nil: true</tt> will not cache nil result:
  218. #
  219. # cache.fetch('foo') { nil }
  220. # cache.fetch('bar', skip_nil: true) { nil }
  221. # cache.exist?('foo') # => true
  222. # cache.exist?('bar') # => false
  223. #
  224. #
  225. # Setting <tt>compress: false</tt> disables compression of the cache entry.
  226. #
  227. # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
  228. # All caches support auto-expiring content after a specified number of
  229. # seconds. This value can be specified as an option to the constructor
  230. # (in which case all entries will be affected), or it can be supplied to
  231. # the +fetch+ or +write+ method to effect just one entry.
  232. #
  233. # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
  234. # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
  235. #
  236. # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
  237. # is of the same version. nil is returned on mismatches despite contents.
  238. # This feature is used to support recyclable cache keys.
  239. #
  240. # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
  241. # a cache entry is used very frequently and is under heavy load. If a
  242. # cache expires and due to heavy load several different processes will try
  243. # to read data natively and then they all will try to write to cache. To
  244. # avoid that case the first process to find an expired cache entry will
  245. # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
  246. # Yes, this process is extending the time for a stale value by another few
  247. # seconds. Because of extended life of the previous cache, other processes
  248. # will continue to use slightly stale data for a just a bit longer. In the
  249. # meantime that first process will go ahead and will write into cache the
  250. # new value. After that all the processes will start getting the new value.
  251. # The key is to keep <tt>:race_condition_ttl</tt> small.
  252. #
  253. # If the process regenerating the entry errors out, the entry will be
  254. # regenerated after the specified number of seconds. Also note that the
  255. # life of stale cache is extended only if it expired recently. Otherwise
  256. # a new value is generated and <tt>:race_condition_ttl</tt> does not play
  257. # any role.
  258. #
  259. # # Set all values to expire after one minute.
  260. # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
  261. #
  262. # cache.write('foo', 'original value')
  263. # val_1 = nil
  264. # val_2 = nil
  265. # sleep 60
  266. #
  267. # Thread.new do
  268. # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
  269. # sleep 1
  270. # 'new value 1'
  271. # end
  272. # end
  273. #
  274. # Thread.new do
  275. # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
  276. # 'new value 2'
  277. # end
  278. # end
  279. #
  280. # cache.fetch('foo') # => "original value"
  281. # sleep 10 # First thread extended the life of cache by another 10 seconds
  282. # cache.fetch('foo') # => "new value 1"
  283. # val_1 # => "new value 1"
  284. # val_2 # => "original value"
  285. #
  286. # Other options will be handled by the specific cache store implementation.
  287. # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
  288. # miss. +options+ will be passed to the #read and #write calls.
  289. #
  290. # For example, MemCacheStore's #write method supports the +:raw+
  291. # option, which tells the memcached server to store all values as strings.
  292. # We can use this option with #fetch too:
  293. #
  294. # cache = ActiveSupport::Cache::MemCacheStore.new
  295. # cache.fetch("foo", force: true, raw: true) do
  296. # :bar
  297. # end
  298. # cache.fetch('foo') # => "bar"
  299. 13 def fetch(name, options = nil, &block)
  300. if block_given?
  301. options = merged_options(options)
  302. key = normalize_key(name, options)
  303. entry = nil
  304. instrument(:read, name, options) do |payload|
  305. cached_entry = read_entry(key, **options) unless options[:force]
  306. entry = handle_expired_entry(cached_entry, key, options)
  307. entry = nil if entry && entry.mismatched?(normalize_version(name, options))
  308. payload[:super_operation] = :fetch if payload
  309. payload[:hit] = !!entry if payload
  310. end
  311. if entry
  312. get_entry_value(entry, name, options)
  313. else
  314. save_block_result_to_cache(name, options, &block)
  315. end
  316. elsif options && options[:force]
  317. raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
  318. else
  319. read(name, options)
  320. end
  321. end
  322. # Reads data from the cache, using the given key. If there is data in
  323. # the cache with the given key, then that data is returned. Otherwise,
  324. # +nil+ is returned.
  325. #
  326. # Note, if data was written with the <tt>:expires_in</tt> or
  327. # <tt>:version</tt> options, both of these conditions are applied before
  328. # the data is returned.
  329. #
  330. # Options are passed to the underlying cache implementation.
  331. 13 def read(name, options = nil)
  332. options = merged_options(options)
  333. key = normalize_key(name, options)
  334. version = normalize_version(name, options)
  335. instrument(:read, name, options) do |payload|
  336. entry = read_entry(key, **options)
  337. if entry
  338. if entry.expired?
  339. delete_entry(key, **options)
  340. payload[:hit] = false if payload
  341. nil
  342. elsif entry.mismatched?(version)
  343. payload[:hit] = false if payload
  344. nil
  345. else
  346. payload[:hit] = true if payload
  347. entry.value
  348. end
  349. else
  350. payload[:hit] = false if payload
  351. nil
  352. end
  353. end
  354. end
  355. # Reads multiple values at once from the cache. Options can be passed
  356. # in the last argument.
  357. #
  358. # Some cache implementation may optimize this method.
  359. #
  360. # Returns a hash mapping the names provided to the values found.
  361. 13 def read_multi(*names)
  362. options = names.extract_options!
  363. options = merged_options(options)
  364. instrument :read_multi, names, options do |payload|
  365. read_multi_entries(names, **options).tap do |results|
  366. payload[:hits] = results.keys
  367. end
  368. end
  369. end
  370. # Cache Storage API to write multiple values at once.
  371. 13 def write_multi(hash, options = nil)
  372. options = merged_options(options)
  373. instrument :write_multi, hash, options do |payload|
  374. entries = hash.each_with_object({}) do |(name, value), memo|
  375. memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
  376. end
  377. write_multi_entries entries, **options
  378. end
  379. end
  380. # Fetches data from the cache, using the given keys. If there is data in
  381. # the cache with the given keys, then that data is returned. Otherwise,
  382. # the supplied block is called for each key for which there was no data,
  383. # and the result will be written to the cache and returned.
  384. # Therefore, you need to pass a block that returns the data to be written
  385. # to the cache. If you do not want to write the cache when the cache is
  386. # not found, use #read_multi.
  387. #
  388. # Returns a hash with the data for each of the names. For example:
  389. #
  390. # cache.write("bim", "bam")
  391. # cache.fetch_multi("bim", "unknown_key") do |key|
  392. # "Fallback value for key: #{key}"
  393. # end
  394. # # => { "bim" => "bam",
  395. # # "unknown_key" => "Fallback value for key: unknown_key" }
  396. #
  397. # Options are passed to the underlying cache implementation. For example:
  398. #
  399. # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
  400. # "buzz"
  401. # end
  402. # # => {"fizz"=>"buzz"}
  403. # cache.read("fizz")
  404. # # => "buzz"
  405. # sleep(6)
  406. # cache.read("fizz")
  407. # # => nil
  408. 13 def fetch_multi(*names)
  409. raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
  410. options = names.extract_options!
  411. options = merged_options(options)
  412. instrument :read_multi, names, options do |payload|
  413. reads = read_multi_entries(names, **options)
  414. writes = {}
  415. ordered = names.index_with do |name|
  416. reads.fetch(name) { writes[name] = yield(name) }
  417. end
  418. payload[:hits] = reads.keys
  419. payload[:super_operation] = :fetch_multi
  420. write_multi(writes, options)
  421. ordered
  422. end
  423. end
  424. # Writes the value to the cache, with the key.
  425. #
  426. # Options are passed to the underlying cache implementation.
  427. 13 def write(name, value, options = nil)
  428. options = merged_options(options)
  429. instrument(:write, name, options) do
  430. entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
  431. write_entry(normalize_key(name, options), entry, **options)
  432. end
  433. end
  434. # Deletes an entry in the cache. Returns +true+ if an entry is deleted.
  435. #
  436. # Options are passed to the underlying cache implementation.
  437. 13 def delete(name, options = nil)
  438. options = merged_options(options)
  439. instrument(:delete, name) do
  440. delete_entry(normalize_key(name, options), **options)
  441. end
  442. end
  443. # Deletes multiple entries in the cache.
  444. #
  445. # Options are passed to the underlying cache implementation.
  446. 13 def delete_multi(names, options = nil)
  447. options = merged_options(options)
  448. names.map! { |key| normalize_key(key, options) }
  449. instrument :delete_multi, names do
  450. delete_multi_entries(names, **options)
  451. end
  452. end
  453. # Returns +true+ if the cache contains an entry for the given key.
  454. #
  455. # Options are passed to the underlying cache implementation.
  456. 13 def exist?(name, options = nil)
  457. options = merged_options(options)
  458. instrument(:exist?, name) do
  459. entry = read_entry(normalize_key(name, options), **options)
  460. (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
  461. end
  462. end
  463. # Deletes all entries with keys matching the pattern.
  464. #
  465. # Options are passed to the underlying cache implementation.
  466. #
  467. # Some implementations may not support this method.
  468. 13 def delete_matched(matcher, options = nil)
  469. raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
  470. end
  471. # Increments an integer value in the cache.
  472. #
  473. # Options are passed to the underlying cache implementation.
  474. #
  475. # Some implementations may not support this method.
  476. 13 def increment(name, amount = 1, options = nil)
  477. raise NotImplementedError.new("#{self.class.name} does not support increment")
  478. end
  479. # Decrements an integer value in the cache.
  480. #
  481. # Options are passed to the underlying cache implementation.
  482. #
  483. # Some implementations may not support this method.
  484. 13 def decrement(name, amount = 1, options = nil)
  485. raise NotImplementedError.new("#{self.class.name} does not support decrement")
  486. end
  487. # Cleanups the cache by removing expired entries.
  488. #
  489. # Options are passed to the underlying cache implementation.
  490. #
  491. # Some implementations may not support this method.
  492. 13 def cleanup(options = nil)
  493. raise NotImplementedError.new("#{self.class.name} does not support cleanup")
  494. end
  495. # Clears the entire cache. Be careful with this method since it could
  496. # affect other processes if shared cache is being used.
  497. #
  498. # The options hash is passed to the underlying cache implementation.
  499. #
  500. # Some implementations may not support this method.
  501. 13 def clear(options = nil)
  502. raise NotImplementedError.new("#{self.class.name} does not support clear")
  503. end
  504. 13 private
  505. # Adds the namespace defined in the options to a pattern designed to
  506. # match keys. Implementations that support delete_matched should call
  507. # this method to translate a pattern that matches names into one that
  508. # matches namespaced keys.
  509. 13 def key_matcher(pattern, options) # :doc:
  510. prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
  511. if prefix
  512. source = pattern.source
  513. if source.start_with?("^")
  514. source = source[1, source.length]
  515. else
  516. source = ".*#{source[0, source.length]}"
  517. end
  518. Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
  519. else
  520. pattern
  521. end
  522. end
  523. # Reads an entry from the cache implementation. Subclasses must implement
  524. # this method.
  525. 13 def read_entry(key, **options)
  526. raise NotImplementedError.new
  527. end
  528. # Writes an entry to the cache implementation. Subclasses must implement
  529. # this method.
  530. 13 def write_entry(key, entry, **options)
  531. raise NotImplementedError.new
  532. end
  533. # Reads multiple entries from the cache implementation. Subclasses MAY
  534. # implement this method.
  535. 13 def read_multi_entries(names, **options)
  536. names.each_with_object({}) do |name, results|
  537. key = normalize_key(name, options)
  538. entry = read_entry(key, **options)
  539. next unless entry
  540. version = normalize_version(name, options)
  541. if entry.expired?
  542. delete_entry(key, **options)
  543. elsif !entry.mismatched?(version)
  544. results[name] = entry.value
  545. end
  546. end
  547. end
  548. # Writes multiple entries to the cache implementation. Subclasses MAY
  549. # implement this method.
  550. 13 def write_multi_entries(hash, **options)
  551. hash.each do |key, entry|
  552. write_entry key, entry, **options
  553. end
  554. end
  555. # Deletes an entry from the cache implementation. Subclasses must
  556. # implement this method.
  557. 13 def delete_entry(key, **options)
  558. raise NotImplementedError.new
  559. end
  560. # Deletes multiples entries in the cache implementation. Subclasses MAY
  561. # implement this method.
  562. 13 def delete_multi_entries(entries, **options)
  563. entries.inject(0) do |sum, key|
  564. if delete_entry(key, **options)
  565. sum + 1
  566. else
  567. sum
  568. end
  569. end
  570. end
  571. # Merges the default options with ones specific to a method call.
  572. 13 def merged_options(call_options)
  573. if call_options
  574. if options.empty?
  575. call_options
  576. else
  577. options.merge(call_options)
  578. end
  579. else
  580. options
  581. end
  582. end
  583. # Expands and namespaces the cache key. May be overridden by
  584. # cache stores to do additional normalization.
  585. 13 def normalize_key(key, options = nil)
  586. namespace_key expanded_key(key), options
  587. end
  588. # Prefix the key with a namespace string:
  589. #
  590. # namespace_key 'foo', namespace: 'cache'
  591. # # => 'cache:foo'
  592. #
  593. # With a namespace block:
  594. #
  595. # namespace_key 'foo', namespace: -> { 'cache' }
  596. # # => 'cache:foo'
  597. 13 def namespace_key(key, options = nil)
  598. options = merged_options(options)
  599. namespace = options[:namespace]
  600. if namespace.respond_to?(:call)
  601. namespace = namespace.call
  602. end
  603. if key && key.encoding != Encoding::UTF_8
  604. key = key.dup.force_encoding(Encoding::UTF_8)
  605. end
  606. if namespace
  607. "#{namespace}:#{key}"
  608. else
  609. key
  610. end
  611. end
  612. # Expands key to be a consistent string value. Invokes +cache_key+ if
  613. # object responds to +cache_key+. Otherwise, +to_param+ method will be
  614. # called. If the key is a Hash, then keys will be sorted alphabetically.
  615. 13 def expanded_key(key)
  616. return key.cache_key.to_s if key.respond_to?(:cache_key)
  617. case key
  618. when Array
  619. if key.size > 1
  620. key.collect { |element| expanded_key(element) }
  621. else
  622. expanded_key(key.first)
  623. end
  624. when Hash
  625. key.collect { |k, v| "#{k}=#{v}" }.sort
  626. else
  627. key
  628. end.to_param
  629. end
  630. 13 def normalize_version(key, options = nil)
  631. (options && options[:version].try(:to_param)) || expanded_version(key)
  632. end
  633. 13 def expanded_version(key)
  634. case
  635. when key.respond_to?(:cache_version) then key.cache_version.to_param
  636. when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param
  637. when key.respond_to?(:to_a) then expanded_version(key.to_a)
  638. end
  639. end
  640. 13 def instrument(operation, key, options = nil)
  641. if logger && logger.debug? && !silence?
  642. logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
  643. end
  644. payload = { key: key }
  645. payload.merge!(options) if options.is_a?(Hash)
  646. ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
  647. end
  648. 13 def handle_expired_entry(entry, key, options)
  649. if entry && entry.expired?
  650. race_ttl = options[:race_condition_ttl].to_i
  651. if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
  652. # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
  653. # for a brief period while the entry is being recalculated.
  654. entry.expires_at = Time.now + race_ttl
  655. write_entry(key, entry, expires_in: race_ttl * 2)
  656. else
  657. delete_entry(key, **options)
  658. end
  659. entry = nil
  660. end
  661. entry
  662. end
  663. 13 def get_entry_value(entry, name, options)
  664. instrument(:fetch_hit, name, options) { }
  665. entry.value
  666. end
  667. 13 def save_block_result_to_cache(name, options)
  668. result = instrument(:generate, name, options) do
  669. yield(name)
  670. end
  671. write(name, result, options) unless result.nil? && options[:skip_nil]
  672. result
  673. end
  674. end
  675. # This class is used to represent cache entries. Cache entries have a value, an optional
  676. # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
  677. # on the cache. The version is used to support the :version option on the cache for rejecting
  678. # mismatches.
  679. #
  680. # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
  681. # using short instance variable names that are lazily defined.
  682. 13 class Entry # :nodoc:
  683. 13 attr_reader :version
  684. 13 DEFAULT_COMPRESS_LIMIT = 1.kilobyte
  685. # Creates a new cache entry for the specified value. Options supported are
  686. # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
  687. 13 def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
  688. @value = value
  689. @version = version
  690. @created_at = Time.now.to_f
  691. @expires_in = expires_in && expires_in.to_f
  692. compress!(compress_threshold) if compress
  693. end
  694. 13 def value
  695. compressed? ? uncompress(@value) : @value
  696. end
  697. 13 def mismatched?(version)
  698. @version && version && @version != version
  699. end
  700. # Checks if the entry is expired. The +expires_in+ parameter can override
  701. # the value set when the entry was created.
  702. 13 def expired?
  703. @expires_in && @created_at + @expires_in <= Time.now.to_f
  704. end
  705. 13 def expires_at
  706. @expires_in ? @created_at + @expires_in : nil
  707. end
  708. 13 def expires_at=(value)
  709. if value
  710. @expires_in = value.to_f - @created_at
  711. else
  712. @expires_in = nil
  713. end
  714. end
  715. # Returns the size of the cached value. This could be less than
  716. # <tt>value.size</tt> if the data is compressed.
  717. 13 def size
  718. case value
  719. when NilClass
  720. 0
  721. when String
  722. @value.bytesize
  723. else
  724. @s ||= Marshal.dump(@value).bytesize
  725. end
  726. end
  727. # Duplicates the value in a class. This is used by cache implementations that don't natively
  728. # serialize entries to protect against accidental cache modifications.
  729. 13 def dup_value!
  730. if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
  731. if @value.is_a?(String)
  732. @value = @value.dup
  733. else
  734. @value = Marshal.load(Marshal.dump(@value))
  735. end
  736. end
  737. end
  738. 13 private
  739. 13 def compress!(compress_threshold)
  740. case @value
  741. when nil, true, false, Numeric
  742. uncompressed_size = 0
  743. when String
  744. uncompressed_size = @value.bytesize
  745. else
  746. serialized = Marshal.dump(@value)
  747. uncompressed_size = serialized.bytesize
  748. end
  749. if uncompressed_size >= compress_threshold
  750. serialized ||= Marshal.dump(@value)
  751. compressed = Zlib::Deflate.deflate(serialized)
  752. if compressed.bytesize < uncompressed_size
  753. @value = compressed
  754. @compressed = true
  755. end
  756. end
  757. end
  758. 13 def compressed?
  759. defined?(@compressed)
  760. end
  761. 13 def uncompress(value)
  762. Marshal.load(Zlib::Inflate.inflate(value))
  763. end
  764. end
  765. end
  766. end

lib/active_support/cache/file_store.rb

0.0% lines covered

147 relevant lines. 0 lines covered and 147 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/marshal"
  3. require "active_support/core_ext/file/atomic"
  4. require "active_support/core_ext/string/conversions"
  5. require "uri/common"
  6. module ActiveSupport
  7. module Cache
  8. # A cache store implementation which stores everything on the filesystem.
  9. #
  10. # FileStore implements the Strategy::LocalCache strategy which implements
  11. # an in-memory cache inside of a block.
  12. class FileStore < Store
  13. prepend Strategy::LocalCache
  14. attr_reader :cache_path
  15. DIR_FORMATTER = "%03X"
  16. FILENAME_MAX_SIZE = 226 # max filename size on file system is 255, minus room for timestamp, pid, and random characters appended by Tempfile (used by atomic write)
  17. FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
  18. GITKEEP_FILES = [".gitkeep", ".keep"].freeze
  19. def initialize(cache_path, options = nil)
  20. super(options)
  21. @cache_path = cache_path.to_s
  22. end
  23. # Advertise cache versioning support.
  24. def self.supports_cache_versioning?
  25. true
  26. end
  27. # Deletes all items from the cache. In this case it deletes all the entries in the specified
  28. # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
  29. # config file when using +FileStore+ because everything in that directory will be deleted.
  30. def clear(options = nil)
  31. root_dirs = (Dir.children(cache_path) - GITKEEP_FILES)
  32. FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
  33. rescue Errno::ENOENT, Errno::ENOTEMPTY
  34. end
  35. # Preemptively iterates through all stored keys and removes the ones which have expired.
  36. def cleanup(options = nil)
  37. options = merged_options(options)
  38. search_dir(cache_path) do |fname|
  39. entry = read_entry(fname, **options)
  40. delete_entry(fname, **options) if entry && entry.expired?
  41. end
  42. end
  43. # Increments an already existing integer value that is stored in the cache.
  44. # If the key is not found nothing is done.
  45. def increment(name, amount = 1, options = nil)
  46. modify_value(name, amount, options)
  47. end
  48. # Decrements an already existing integer value that is stored in the cache.
  49. # If the key is not found nothing is done.
  50. def decrement(name, amount = 1, options = nil)
  51. modify_value(name, -amount, options)
  52. end
  53. def delete_matched(matcher, options = nil)
  54. options = merged_options(options)
  55. instrument(:delete_matched, matcher.inspect) do
  56. matcher = key_matcher(matcher, options)
  57. search_dir(cache_path) do |path|
  58. key = file_path_key(path)
  59. delete_entry(path, **options) if key.match(matcher)
  60. end
  61. end
  62. end
  63. private
  64. def read_entry(key, **options)
  65. if File.exist?(key)
  66. entry = File.open(key) { |f| Marshal.load(f) }
  67. entry if entry.is_a?(Cache::Entry)
  68. end
  69. rescue => e
  70. logger.error("FileStoreError (#{e}): #{e.message}") if logger
  71. nil
  72. end
  73. def write_entry(key, entry, **options)
  74. return false if options[:unless_exist] && File.exist?(key)
  75. ensure_cache_path(File.dirname(key))
  76. File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
  77. true
  78. end
  79. def delete_entry(key, **options)
  80. if File.exist?(key)
  81. begin
  82. File.delete(key)
  83. delete_empty_directories(File.dirname(key))
  84. true
  85. rescue => e
  86. # Just in case the error was caused by another process deleting the file first.
  87. raise e if File.exist?(key)
  88. false
  89. end
  90. end
  91. end
  92. # Lock a file for a block so only one process can modify it at a time.
  93. def lock_file(file_name, &block)
  94. if File.exist?(file_name)
  95. File.open(file_name, "r+") do |f|
  96. f.flock File::LOCK_EX
  97. yield
  98. ensure
  99. f.flock File::LOCK_UN
  100. end
  101. else
  102. yield
  103. end
  104. end
  105. # Translate a key into a file path.
  106. def normalize_key(key, options)
  107. key = super
  108. fname = URI.encode_www_form_component(key)
  109. if fname.size > FILEPATH_MAX_SIZE
  110. fname = ActiveSupport::Digest.hexdigest(key)
  111. end
  112. hash = Zlib.adler32(fname)
  113. hash, dir_1 = hash.divmod(0x1000)
  114. dir_2 = hash.modulo(0x1000)
  115. # Make sure file name doesn't exceed file system limits.
  116. if fname.length < FILENAME_MAX_SIZE
  117. fname_paths = fname
  118. else
  119. fname_paths = []
  120. begin
  121. fname_paths << fname[0, FILENAME_MAX_SIZE]
  122. fname = fname[FILENAME_MAX_SIZE..-1]
  123. end until fname.blank?
  124. end
  125. File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths)
  126. end
  127. # Translate a file path into a key.
  128. def file_path_key(path)
  129. fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last
  130. URI.decode_www_form_component(fname, Encoding::UTF_8)
  131. end
  132. # Delete empty directories in the cache.
  133. def delete_empty_directories(dir)
  134. return if File.realpath(dir) == File.realpath(cache_path)
  135. if Dir.children(dir).empty?
  136. Dir.delete(dir) rescue nil
  137. delete_empty_directories(File.dirname(dir))
  138. end
  139. end
  140. # Make sure a file path's directories exist.
  141. def ensure_cache_path(path)
  142. FileUtils.makedirs(path) unless File.exist?(path)
  143. end
  144. def search_dir(dir, &callback)
  145. return if !File.exist?(dir)
  146. Dir.each_child(dir) do |d|
  147. name = File.join(dir, d)
  148. if File.directory?(name)
  149. search_dir(name, &callback)
  150. else
  151. callback.call name
  152. end
  153. end
  154. end
  155. # Modifies the amount of an already existing integer value that is stored in the cache.
  156. # If the key is not found nothing is done.
  157. def modify_value(name, amount, options)
  158. file_name = normalize_key(name, options)
  159. lock_file(file_name) do
  160. options = merged_options(options)
  161. if num = read(name, options)
  162. num = num.to_i + amount
  163. write(name, num, options)
  164. num
  165. end
  166. end
  167. end
  168. end
  169. end
  170. end

lib/active_support/cache/mem_cache_store.rb

0.0% lines covered

130 relevant lines. 0 lines covered and 130 lines missed.
    
  1. # frozen_string_literal: true
  2. begin
  3. require "dalli"
  4. rescue LoadError => e
  5. $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
  6. raise e
  7. end
  8. require "active_support/core_ext/enumerable"
  9. require "active_support/core_ext/marshal"
  10. require "active_support/core_ext/array/extract_options"
  11. module ActiveSupport
  12. module Cache
  13. # A cache store implementation which stores data in Memcached:
  14. # https://memcached.org
  15. #
  16. # This is currently the most popular cache store for production websites.
  17. #
  18. # Special features:
  19. # - Clustering and load balancing. One can specify multiple memcached servers,
  20. # and MemCacheStore will load balance between all available servers. If a
  21. # server goes down, then MemCacheStore will ignore it until it comes back up.
  22. #
  23. # MemCacheStore implements the Strategy::LocalCache strategy which implements
  24. # an in-memory cache inside of a block.
  25. class MemCacheStore < Store
  26. # Provide support for raw values in the local cache strategy.
  27. module LocalCacheWithRaw # :nodoc:
  28. private
  29. def write_entry(key, entry, **options)
  30. if options[:raw] && local_cache
  31. raw_entry = Entry.new(entry.value.to_s)
  32. raw_entry.expires_at = entry.expires_at
  33. super(key, raw_entry, **options)
  34. else
  35. super
  36. end
  37. end
  38. end
  39. # Advertise cache versioning support.
  40. def self.supports_cache_versioning?
  41. true
  42. end
  43. prepend Strategy::LocalCache
  44. prepend LocalCacheWithRaw
  45. ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
  46. # Creates a new Dalli::Client instance with specified addresses and options.
  47. # By default address is equal localhost:11211.
  48. #
  49. # ActiveSupport::Cache::MemCacheStore.build_mem_cache
  50. # # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil>
  51. # ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290')
  52. # # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil>
  53. def self.build_mem_cache(*addresses) # :nodoc:
  54. addresses = addresses.flatten
  55. options = addresses.extract_options!
  56. addresses = ["localhost:11211"] if addresses.empty?
  57. pool_options = retrieve_pool_options(options)
  58. if pool_options.empty?
  59. Dalli::Client.new(addresses, options)
  60. else
  61. ensure_connection_pool_added!
  62. ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
  63. end
  64. end
  65. # Creates a new MemCacheStore object, with the given memcached server
  66. # addresses. Each address is either a host name, or a host-with-port string
  67. # in the form of "host_name:port". For example:
  68. #
  69. # ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
  70. #
  71. # If no addresses are specified, then MemCacheStore will connect to
  72. # localhost port 11211 (the default memcached port).
  73. def initialize(*addresses)
  74. addresses = addresses.flatten
  75. options = addresses.extract_options!
  76. super(options)
  77. unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
  78. raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
  79. end
  80. if addresses.first.is_a?(Dalli::Client)
  81. @data = addresses.first
  82. else
  83. mem_cache_options = options.dup
  84. UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
  85. @data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
  86. end
  87. end
  88. # Increment a cached value. This method uses the memcached incr atomic
  89. # operator and can only be used on values written with the :raw option.
  90. # Calling it on a value not stored with :raw will initialize that value
  91. # to zero.
  92. def increment(name, amount = 1, options = nil)
  93. options = merged_options(options)
  94. instrument(:increment, name, amount: amount) do
  95. rescue_error_with nil do
  96. @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
  97. end
  98. end
  99. end
  100. # Decrement a cached value. This method uses the memcached decr atomic
  101. # operator and can only be used on values written with the :raw option.
  102. # Calling it on a value not stored with :raw will initialize that value
  103. # to zero.
  104. def decrement(name, amount = 1, options = nil)
  105. options = merged_options(options)
  106. instrument(:decrement, name, amount: amount) do
  107. rescue_error_with nil do
  108. @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
  109. end
  110. end
  111. end
  112. # Clear the entire cache on all memcached servers. This method should
  113. # be used with care when shared cache is being used.
  114. def clear(options = nil)
  115. rescue_error_with(nil) { @data.with { |c| c.flush_all } }
  116. end
  117. # Get the statistics from the memcached servers.
  118. def stats
  119. @data.with { |c| c.stats }
  120. end
  121. private
  122. # Read an entry from the cache.
  123. def read_entry(key, **options)
  124. rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) }
  125. end
  126. # Write an entry to the cache.
  127. def write_entry(key, entry, **options)
  128. method = options && options[:unless_exist] ? :add : :set
  129. value = options[:raw] ? entry.value.to_s : entry
  130. expires_in = options[:expires_in].to_i
  131. if expires_in > 0 && !options[:raw]
  132. # Set the memcache expire a few minutes in the future to support race condition ttls on read
  133. expires_in += 5.minutes
  134. end
  135. rescue_error_with false do
  136. # The value "compress: false" prevents duplicate compression within Dalli.
  137. @data.with { |c| c.send(method, key, value, expires_in, **options, compress: false) }
  138. end
  139. end
  140. # Reads multiple entries from the cache implementation.
  141. def read_multi_entries(names, **options)
  142. keys_to_names = names.index_by { |name| normalize_key(name, options) }
  143. raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
  144. values = {}
  145. raw_values.each do |key, value|
  146. entry = deserialize_entry(value)
  147. unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
  148. values[keys_to_names[key]] = entry.value
  149. end
  150. end
  151. values
  152. end
  153. # Delete an entry from the cache.
  154. def delete_entry(key, **options)
  155. rescue_error_with(false) { @data.with { |c| c.delete(key) } }
  156. end
  157. # Memcache keys are binaries. So we need to force their encoding to binary
  158. # before applying the regular expression to ensure we are escaping all
  159. # characters properly.
  160. def normalize_key(key, options)
  161. key = super.dup
  162. key = key.force_encoding(Encoding::ASCII_8BIT)
  163. key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
  164. key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250
  165. key
  166. end
  167. def deserialize_entry(entry)
  168. if entry
  169. entry.is_a?(Entry) ? entry : Entry.new(entry, compress: false)
  170. end
  171. end
  172. def rescue_error_with(fallback)
  173. yield
  174. rescue Dalli::DalliError => e
  175. logger.error("DalliError (#{e}): #{e.message}") if logger
  176. fallback
  177. end
  178. end
  179. end
  180. end

lib/active_support/cache/memory_store.rb

0.0% lines covered

127 relevant lines. 0 lines covered and 127 lines missed.
    
  1. # frozen_string_literal: true
  2. require "monitor"
  3. module ActiveSupport
  4. module Cache
  5. # A cache store implementation which stores everything into memory in the
  6. # same process. If you're running multiple Ruby on Rails server processes
  7. # (which is the case if you're using Phusion Passenger or puma clustered mode),
  8. # then this means that Rails server process instances won't be able
  9. # to share cache data with each other and this may not be the most
  10. # appropriate cache in that scenario.
  11. #
  12. # This cache has a bounded size specified by the :size options to the
  13. # initializer (default is 32Mb). When the cache exceeds the allotted size,
  14. # a cleanup will occur which tries to prune the cache down to three quarters
  15. # of the maximum size by removing the least recently used entries.
  16. #
  17. # MemoryStore is thread-safe.
  18. class MemoryStore < Store
  19. def initialize(options = nil)
  20. options ||= {}
  21. super(options)
  22. @data = {}
  23. @max_size = options[:size] || 32.megabytes
  24. @max_prune_time = options[:max_prune_time] || 2
  25. @cache_size = 0
  26. @monitor = Monitor.new
  27. @pruning = false
  28. end
  29. # Advertise cache versioning support.
  30. def self.supports_cache_versioning?
  31. true
  32. end
  33. # Delete all data stored in a given cache store.
  34. def clear(options = nil)
  35. synchronize do
  36. @data.clear
  37. @cache_size = 0
  38. end
  39. end
  40. # Preemptively iterates through all stored keys and removes the ones which have expired.
  41. def cleanup(options = nil)
  42. options = merged_options(options)
  43. instrument(:cleanup, size: @data.size) do
  44. keys = synchronize { @data.keys }
  45. keys.each do |key|
  46. entry = @data[key]
  47. delete_entry(key, **options) if entry && entry.expired?
  48. end
  49. end
  50. end
  51. # To ensure entries fit within the specified memory prune the cache by removing the least
  52. # recently accessed entries.
  53. def prune(target_size, max_time = nil)
  54. return if pruning?
  55. @pruning = true
  56. begin
  57. start_time = Concurrent.monotonic_time
  58. cleanup
  59. instrument(:prune, target_size, from: @cache_size) do
  60. keys = synchronize { @data.keys }
  61. keys.each do |key|
  62. delete_entry(key, **options)
  63. return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
  64. end
  65. end
  66. ensure
  67. @pruning = false
  68. end
  69. end
  70. # Returns true if the cache is currently being pruned.
  71. def pruning?
  72. @pruning
  73. end
  74. # Increment an integer value in the cache.
  75. def increment(name, amount = 1, options = nil)
  76. modify_value(name, amount, options)
  77. end
  78. # Decrement an integer value in the cache.
  79. def decrement(name, amount = 1, options = nil)
  80. modify_value(name, -amount, options)
  81. end
  82. # Deletes cache entries if the cache key matches a given pattern.
  83. def delete_matched(matcher, options = nil)
  84. options = merged_options(options)
  85. instrument(:delete_matched, matcher.inspect) do
  86. matcher = key_matcher(matcher, options)
  87. keys = synchronize { @data.keys }
  88. keys.each do |key|
  89. delete_entry(key, **options) if key.match(matcher)
  90. end
  91. end
  92. end
  93. def inspect # :nodoc:
  94. "#<#{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
  95. end
  96. # Synchronize calls to the cache. This should be called wherever the underlying cache implementation
  97. # is not thread safe.
  98. def synchronize(&block) # :nodoc:
  99. @monitor.synchronize(&block)
  100. end
  101. private
  102. PER_ENTRY_OVERHEAD = 240
  103. def cached_size(key, entry)
  104. key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
  105. end
  106. def read_entry(key, **options)
  107. entry = nil
  108. synchronize do
  109. entry = @data.delete(key)
  110. if entry
  111. @data[key] = entry
  112. entry = entry.dup
  113. end
  114. end
  115. entry&.dup_value!
  116. entry
  117. end
  118. def write_entry(key, entry, **options)
  119. entry.dup_value!
  120. synchronize do
  121. return false if options[:unless_exist] && @data.key?(key)
  122. old_entry = @data.delete(key)
  123. if old_entry
  124. @cache_size -= (old_entry.size - entry.size)
  125. else
  126. @cache_size += cached_size(key, entry)
  127. end
  128. @data[key] = entry
  129. prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
  130. true
  131. end
  132. end
  133. def delete_entry(key, **options)
  134. synchronize do
  135. entry = @data.delete(key)
  136. @cache_size -= cached_size(key, entry) if entry
  137. !!entry
  138. end
  139. end
  140. def modify_value(name, amount, options)
  141. options = merged_options(options)
  142. synchronize do
  143. if num = read(name, options)
  144. num = num.to_i + amount
  145. write(name, num, options)
  146. num
  147. end
  148. end
  149. end
  150. end
  151. end
  152. end

lib/active_support/cache/null_store.rb

0.0% lines covered

29 relevant lines. 0 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. module Cache
  4. # A cache store implementation which doesn't actually store anything. Useful in
  5. # development and test environments where you don't want caching turned on but
  6. # need to go through the caching interface.
  7. #
  8. # This cache does implement the local cache strategy, so values will actually
  9. # be cached inside blocks that utilize this strategy. See
  10. # ActiveSupport::Cache::Strategy::LocalCache for more details.
  11. class NullStore < Store
  12. prepend Strategy::LocalCache
  13. # Advertise cache versioning support.
  14. def self.supports_cache_versioning?
  15. true
  16. end
  17. def clear(options = nil)
  18. end
  19. def cleanup(options = nil)
  20. end
  21. def increment(name, amount = 1, options = nil)
  22. end
  23. def decrement(name, amount = 1, options = nil)
  24. end
  25. def delete_matched(matcher, options = nil)
  26. end
  27. private
  28. def read_entry(key, **options)
  29. end
  30. def write_entry(key, entry, **options)
  31. true
  32. end
  33. def delete_entry(key, **options)
  34. false
  35. end
  36. end
  37. end
  38. end

lib/active_support/cache/redis_cache_store.rb

30.24% lines covered

205 relevant lines. 62 lines covered and 143 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 begin
  3. 3 gem "redis", ">= 4.0.1"
  4. 3 require "redis"
  5. 3 require "redis/distributed"
  6. rescue LoadError
  7. warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
  8. raise
  9. end
  10. # Prefer the hiredis driver but don't require it.
  11. 3 begin
  12. 3 require "redis/connection/hiredis"
  13. rescue LoadError
  14. end
  15. 3 require "digest/sha2"
  16. 3 require "active_support/core_ext/marshal"
  17. 3 module ActiveSupport
  18. 3 module Cache
  19. 3 module ConnectionPoolLike
  20. 3 def with
  21. yield self
  22. end
  23. end
  24. 3 ::Redis.include(ConnectionPoolLike)
  25. 3 ::Redis::Distributed.include(ConnectionPoolLike)
  26. # Redis cache store.
  27. #
  28. # Deployment note: Take care to use a *dedicated Redis cache* rather
  29. # than pointing this at your existing Redis server. It won't cope well
  30. # with mixed usage patterns and it won't expire cache entries by default.
  31. #
  32. # Redis cache server setup guide: https://redis.io/topics/lru-cache
  33. #
  34. # * Supports vanilla Redis, hiredis, and Redis::Distributed.
  35. # * Supports Memcached-like sharding across Redises with Redis::Distributed.
  36. # * Fault tolerant. If the Redis server is unavailable, no exceptions are
  37. # raised. Cache fetches are all misses and writes are dropped.
  38. # * Local cache. Hot in-memory primary cache within block/middleware scope.
  39. # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
  40. # 4.0.1+ for distributed mget support.
  41. # * +delete_matched+ support for Redis KEYS globs.
  42. 3 class RedisCacheStore < Store
  43. # Keys are truncated with their own SHA2 digest if they exceed 1kB
  44. 3 MAX_KEY_BYTESIZE = 1024
  45. 3 DEFAULT_REDIS_OPTIONS = {
  46. connect_timeout: 20,
  47. read_timeout: 1,
  48. write_timeout: 1,
  49. reconnect_attempts: 0,
  50. }
  51. 3 DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
  52. if logger
  53. logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
  54. end
  55. end
  56. # The maximum number of entries to receive per SCAN call.
  57. 3 SCAN_BATCH_SIZE = 1000
  58. 3 private_constant :SCAN_BATCH_SIZE
  59. # Advertise cache versioning support.
  60. 3 def self.supports_cache_versioning?
  61. true
  62. end
  63. # Support raw values in the local cache strategy.
  64. 3 module LocalCacheWithRaw # :nodoc:
  65. 3 private
  66. 3 def write_entry(key, entry, **options)
  67. if options[:raw] && local_cache
  68. raw_entry = Entry.new(serialize_entry(entry, raw: true))
  69. raw_entry.expires_at = entry.expires_at
  70. super(key, raw_entry, **options)
  71. else
  72. super
  73. end
  74. end
  75. 3 def write_multi_entries(entries, **options)
  76. if options[:raw] && local_cache
  77. raw_entries = entries.map do |key, entry|
  78. raw_entry = Entry.new(serialize_entry(entry, raw: true))
  79. raw_entry.expires_at = entry.expires_at
  80. end.to_h
  81. super(raw_entries, **options)
  82. else
  83. super
  84. end
  85. end
  86. end
  87. 3 prepend Strategy::LocalCache
  88. 3 prepend LocalCacheWithRaw
  89. 3 class << self
  90. # Factory method to create a new Redis instance.
  91. #
  92. # Handles four options: :redis block, :redis instance, single :url
  93. # string, and multiple :url strings.
  94. #
  95. # Option Class Result
  96. # :redis Proc -> options[:redis].call
  97. # :redis Object -> options[:redis]
  98. # :url String -> Redis.new(url: …)
  99. # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
  100. #
  101. 3 def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
  102. urls = Array(url)
  103. if redis.is_a?(Proc)
  104. redis.call
  105. elsif redis
  106. redis
  107. elsif urls.size > 1
  108. build_redis_distributed_client urls: urls, **redis_options
  109. else
  110. build_redis_client url: urls.first, **redis_options
  111. end
  112. end
  113. 3 private
  114. 3 def build_redis_distributed_client(urls:, **redis_options)
  115. ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
  116. urls.each { |u| dist.add_node url: u }
  117. end
  118. end
  119. 3 def build_redis_client(url:, **redis_options)
  120. ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
  121. end
  122. end
  123. 3 attr_reader :redis_options
  124. 3 attr_reader :max_key_bytesize
  125. # Creates a new Redis cache store.
  126. #
  127. # Handles four options: :redis block, :redis instance, single :url
  128. # string, and multiple :url strings.
  129. #
  130. # Option Class Result
  131. # :redis Proc -> options[:redis].call
  132. # :redis Object -> options[:redis]
  133. # :url String -> Redis.new(url: …)
  134. # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
  135. #
  136. # No namespace is set by default. Provide one if the Redis cache
  137. # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
  138. #
  139. # Compression is enabled by default with a 1kB threshold, so cached
  140. # values larger than 1kB are automatically compressed. Disable by
  141. # passing <tt>compress: false</tt> or change the threshold by passing
  142. # <tt>compress_threshold: 4.kilobytes</tt>.
  143. #
  144. # No expiry is set on cache entries by default. Redis is expected to
  145. # be configured with an eviction policy that automatically deletes
  146. # least-recently or -frequently used keys when it reaches max memory.
  147. # See https://redis.io/topics/lru-cache for cache server setup.
  148. #
  149. # Race condition TTL is not set by default. This can be used to avoid
  150. # "thundering herd" cache writes when hot cache entries are expired.
  151. # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
  152. 3 def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
  153. @redis_options = redis_options
  154. @max_key_bytesize = MAX_KEY_BYTESIZE
  155. @error_handler = error_handler
  156. super namespace: namespace,
  157. compress: compress, compress_threshold: compress_threshold,
  158. expires_in: expires_in, race_condition_ttl: race_condition_ttl
  159. end
  160. 3 def redis
  161. @redis ||= begin
  162. pool_options = self.class.send(:retrieve_pool_options, redis_options)
  163. if pool_options.any?
  164. self.class.send(:ensure_connection_pool_added!)
  165. ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
  166. else
  167. self.class.build_redis(**redis_options)
  168. end
  169. end
  170. end
  171. 3 def inspect
  172. instance = @redis || @redis_options
  173. "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
  174. end
  175. # Cache Store API implementation.
  176. #
  177. # Read multiple values at once. Returns a hash of requested keys ->
  178. # fetched values.
  179. 3 def read_multi(*names)
  180. if mget_capable?
  181. instrument(:read_multi, names, options) do |payload|
  182. read_multi_mget(*names).tap do |results|
  183. payload[:hits] = results.keys
  184. end
  185. end
  186. else
  187. super
  188. end
  189. end
  190. # Cache Store API implementation.
  191. #
  192. # Supports Redis KEYS glob patterns:
  193. #
  194. # h?llo matches hello, hallo and hxllo
  195. # h*llo matches hllo and heeeello
  196. # h[ae]llo matches hello and hallo, but not hillo
  197. # h[^e]llo matches hallo, hbllo, ... but not hello
  198. # h[a-b]llo matches hallo and hbllo
  199. #
  200. # Use \ to escape special characters if you want to match them verbatim.
  201. #
  202. # See https://redis.io/commands/KEYS for more.
  203. #
  204. # Failsafe: Raises errors.
  205. 3 def delete_matched(matcher, options = nil)
  206. instrument :delete_matched, matcher do
  207. unless String === matcher
  208. raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
  209. end
  210. redis.with do |c|
  211. pattern = namespace_key(matcher, options)
  212. cursor = "0"
  213. # Fetch keys in batches using SCAN to avoid blocking the Redis server.
  214. nodes = c.respond_to?(:nodes) ? c.nodes : [c]
  215. nodes.each do |node|
  216. begin
  217. cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
  218. node.del(*keys) unless keys.empty?
  219. end until cursor == "0"
  220. end
  221. end
  222. end
  223. end
  224. # Cache Store API implementation.
  225. #
  226. # Increment a cached value. This method uses the Redis incr atomic
  227. # operator and can only be used on values written with the :raw option.
  228. # Calling it on a value not stored with :raw will initialize that value
  229. # to zero.
  230. #
  231. # Failsafe: Raises errors.
  232. 3 def increment(name, amount = 1, options = nil)
  233. instrument :increment, name, amount: amount do
  234. failsafe :increment do
  235. options = merged_options(options)
  236. key = normalize_key(name, options)
  237. redis.with do |c|
  238. c.incrby(key, amount).tap do
  239. write_key_expiry(c, key, options)
  240. end
  241. end
  242. end
  243. end
  244. end
  245. # Cache Store API implementation.
  246. #
  247. # Decrement a cached value. This method uses the Redis decr atomic
  248. # operator and can only be used on values written with the :raw option.
  249. # Calling it on a value not stored with :raw will initialize that value
  250. # to zero.
  251. #
  252. # Failsafe: Raises errors.
  253. 3 def decrement(name, amount = 1, options = nil)
  254. instrument :decrement, name, amount: amount do
  255. failsafe :decrement do
  256. options = merged_options(options)
  257. key = normalize_key(name, options)
  258. redis.with do |c|
  259. c.decrby(key, amount).tap do
  260. write_key_expiry(c, key, options)
  261. end
  262. end
  263. end
  264. end
  265. end
  266. # Cache Store API implementation.
  267. #
  268. # Removes expired entries. Handled natively by Redis least-recently-/
  269. # least-frequently-used expiry, so manual cleanup is not supported.
  270. 3 def cleanup(options = nil)
  271. super
  272. end
  273. # Clear the entire cache on all Redis servers. Safe to use on
  274. # shared servers if the cache is namespaced.
  275. #
  276. # Failsafe: Raises errors.
  277. 3 def clear(options = nil)
  278. failsafe :clear do
  279. if namespace = merged_options(options)[:namespace]
  280. delete_matched "*", namespace: namespace
  281. else
  282. redis.with { |c| c.flushdb }
  283. end
  284. end
  285. end
  286. 3 def mget_capable? #:nodoc:
  287. set_redis_capabilities unless defined? @mget_capable
  288. @mget_capable
  289. end
  290. 3 def mset_capable? #:nodoc:
  291. set_redis_capabilities unless defined? @mset_capable
  292. @mset_capable
  293. end
  294. 3 private
  295. 3 def set_redis_capabilities
  296. case redis
  297. when Redis::Distributed
  298. @mget_capable = true
  299. @mset_capable = false
  300. else
  301. @mget_capable = true
  302. @mset_capable = true
  303. end
  304. end
  305. # Store provider interface:
  306. # Read an entry from the cache.
  307. 3 def read_entry(key, **options)
  308. failsafe :read_entry do
  309. raw = options&.fetch(:raw, false)
  310. deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
  311. end
  312. end
  313. 3 def read_multi_entries(names, **options)
  314. if mget_capable?
  315. read_multi_mget(*names, **options)
  316. else
  317. super
  318. end
  319. end
  320. 3 def read_multi_mget(*names)
  321. options = names.extract_options!
  322. options = merged_options(options)
  323. return {} if names == []
  324. raw = options&.fetch(:raw, false)
  325. keys = names.map { |name| normalize_key(name, options) }
  326. values = failsafe(:read_multi_mget, returning: {}) do
  327. redis.with { |c| c.mget(*keys) }
  328. end
  329. names.zip(values).each_with_object({}) do |(name, value), results|
  330. if value
  331. entry = deserialize_entry(value, raw: raw)
  332. unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
  333. results[name] = entry.value
  334. end
  335. end
  336. end
  337. end
  338. # Write an entry to the cache.
  339. #
  340. # Requires Redis 2.6.12+ for extended SET options.
  341. 3 def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
  342. serialized_entry = serialize_entry(entry, raw: raw)
  343. # If race condition TTL is in use, ensure that cache entries
  344. # stick around a bit longer after they would have expired
  345. # so we can purposefully serve stale entries.
  346. if race_condition_ttl && expires_in && expires_in > 0 && !raw
  347. expires_in += 5.minutes
  348. end
  349. failsafe :write_entry, returning: false do
  350. if unless_exist || expires_in
  351. modifiers = {}
  352. modifiers[:nx] = unless_exist
  353. modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
  354. redis.with { |c| c.set key, serialized_entry, **modifiers }
  355. else
  356. redis.with { |c| c.set key, serialized_entry }
  357. end
  358. end
  359. end
  360. 3 def write_key_expiry(client, key, options)
  361. if options[:expires_in] && client.ttl(key).negative?
  362. client.expire key, options[:expires_in].to_i
  363. end
  364. end
  365. # Delete an entry from the cache.
  366. 3 def delete_entry(key, options)
  367. failsafe :delete_entry, returning: false do
  368. redis.with { |c| c.del key }
  369. end
  370. end
  371. # Deletes multiple entries in the cache. Returns the number of entries deleted.
  372. 3 def delete_multi_entries(entries, **_options)
  373. redis.with { |c| c.del(entries) }
  374. end
  375. # Nonstandard store provider API to write multiple values at once.
  376. 3 def write_multi_entries(entries, expires_in: nil, **options)
  377. if entries.any?
  378. if mset_capable? && expires_in.nil?
  379. failsafe :write_multi_entries do
  380. redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
  381. end
  382. else
  383. super
  384. end
  385. end
  386. end
  387. # Truncate keys that exceed 1kB.
  388. 3 def normalize_key(key, options)
  389. truncate_key super&.b
  390. end
  391. 3 def truncate_key(key)
  392. if key && key.bytesize > max_key_bytesize
  393. suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
  394. truncate_at = max_key_bytesize - suffix.bytesize
  395. "#{key.byteslice(0, truncate_at)}#{suffix}"
  396. else
  397. key
  398. end
  399. end
  400. 3 def deserialize_entry(serialized_entry, raw:)
  401. if serialized_entry
  402. if raw
  403. Entry.new(serialized_entry, compress: false)
  404. else
  405. Marshal.load(serialized_entry)
  406. end
  407. end
  408. end
  409. 3 def serialize_entry(entry, raw: false)
  410. if raw
  411. entry.value.to_s
  412. else
  413. Marshal.dump(entry)
  414. end
  415. end
  416. 3 def serialize_entries(entries, raw: false)
  417. entries.transform_values do |entry|
  418. serialize_entry entry, raw: raw
  419. end
  420. end
  421. 3 def failsafe(method, returning: nil)
  422. yield
  423. rescue ::Redis::BaseError => e
  424. handle_exception exception: e, method: method, returning: returning
  425. returning
  426. end
  427. 3 def handle_exception(exception:, method:, returning:)
  428. if @error_handler
  429. @error_handler.(method: method, exception: exception, returning: returning)
  430. end
  431. rescue => failsafe
  432. warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
  433. end
  434. end
  435. end
  436. end

lib/active_support/cache/strategy/local_cache.rb

37.14% lines covered

105 relevant lines. 39 lines covered and 66 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 require "active_support/core_ext/string/inflections"
  3. 4 require "active_support/per_thread_registry"
  4. 4 module ActiveSupport
  5. 4 module Cache
  6. 4 module Strategy
  7. # Caches that implement LocalCache will be backed by an in-memory cache for the
  8. # duration of a block. Repeated calls to the cache for the same key will hit the
  9. # in-memory cache for faster access.
  10. 4 module LocalCache
  11. 4 autoload :Middleware, "active_support/cache/strategy/local_cache_middleware"
  12. # Class for storing and registering the local caches.
  13. 4 class LocalCacheRegistry # :nodoc:
  14. 4 extend ActiveSupport::PerThreadRegistry
  15. 4 def initialize
  16. @registry = {}
  17. end
  18. 4 def cache_for(local_cache_key)
  19. @registry[local_cache_key]
  20. end
  21. 4 def set_cache_for(local_cache_key, value)
  22. @registry[local_cache_key] = value
  23. end
  24. 4 def self.set_cache_for(l, v); instance.set_cache_for l, v; end
  25. 4 def self.cache_for(l); instance.cache_for l; end
  26. end
  27. # Simple memory backed cache. This cache is not thread safe and is intended only
  28. # for serving as a temporary memory cache for a single thread.
  29. 4 class LocalStore < Store
  30. 4 def initialize
  31. super
  32. @data = {}
  33. end
  34. # Don't allow synchronizing since it isn't thread safe.
  35. 4 def synchronize # :nodoc:
  36. yield
  37. end
  38. 4 def clear(options = nil)
  39. @data.clear
  40. end
  41. 4 def read_entry(key, **options)
  42. @data[key]
  43. end
  44. 4 def read_multi_entries(keys, **options)
  45. values = {}
  46. keys.each do |name|
  47. entry = read_entry(name, **options)
  48. values[name] = entry.value if entry
  49. end
  50. values
  51. end
  52. 4 def write_entry(key, entry, **options)
  53. entry.dup_value!
  54. @data[key] = entry
  55. true
  56. end
  57. 4 def delete_entry(key, **options)
  58. !!@data.delete(key)
  59. end
  60. 4 def fetch_entry(key, options = nil) # :nodoc:
  61. entry = @data.fetch(key) { @data[key] = yield }
  62. dup_entry = entry.dup
  63. dup_entry&.dup_value!
  64. dup_entry
  65. end
  66. end
  67. # Use a local cache for the duration of block.
  68. 4 def with_local_cache
  69. use_temporary_local_cache(LocalStore.new) { yield }
  70. end
  71. # Middleware class can be inserted as a Rack handler to be local cache for the
  72. # duration of request.
  73. 4 def middleware
  74. @middleware ||= Middleware.new(
  75. "ActiveSupport::Cache::Strategy::LocalCache",
  76. local_cache_key)
  77. end
  78. 4 def clear(**options) # :nodoc:
  79. return super unless cache = local_cache
  80. cache.clear(options)
  81. super
  82. end
  83. 4 def cleanup(**options) # :nodoc:
  84. return super unless cache = local_cache
  85. cache.clear
  86. super
  87. end
  88. 4 def increment(name, amount = 1, **options) # :nodoc:
  89. return super unless local_cache
  90. value = bypass_local_cache { super }
  91. write_cache_value(name, value, **options)
  92. value
  93. end
  94. 4 def decrement(name, amount = 1, **options) # :nodoc:
  95. return super unless local_cache
  96. value = bypass_local_cache { super }
  97. write_cache_value(name, value, **options)
  98. value
  99. end
  100. 4 private
  101. 4 def read_entry(key, **options)
  102. if cache = local_cache
  103. cache.fetch_entry(key) { super }
  104. else
  105. super
  106. end
  107. end
  108. 4 def read_multi_entries(keys, **options)
  109. return super unless local_cache
  110. local_entries = local_cache.read_multi_entries(keys, **options)
  111. missed_keys = keys - local_entries.keys
  112. if missed_keys.any?
  113. local_entries.merge!(super(missed_keys, **options))
  114. else
  115. local_entries
  116. end
  117. end
  118. 4 def write_entry(key, entry, **options)
  119. if options[:unless_exist]
  120. local_cache.delete_entry(key, **options) if local_cache
  121. else
  122. local_cache.write_entry(key, entry, **options) if local_cache
  123. end
  124. super
  125. end
  126. 4 def delete_entry(key, **options)
  127. local_cache.delete_entry(key, **options) if local_cache
  128. super
  129. end
  130. 4 def write_cache_value(name, value, **options)
  131. name = normalize_key(name, options)
  132. cache = local_cache
  133. cache.mute do
  134. if value
  135. cache.write(name, value, options)
  136. else
  137. cache.delete(name, **options)
  138. end
  139. end
  140. end
  141. 4 def local_cache_key
  142. @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
  143. end
  144. 4 def local_cache
  145. LocalCacheRegistry.cache_for(local_cache_key)
  146. end
  147. 4 def bypass_local_cache
  148. use_temporary_local_cache(nil) { yield }
  149. end
  150. 4 def use_temporary_local_cache(temporary_cache)
  151. save_cache = LocalCacheRegistry.cache_for(local_cache_key)
  152. begin
  153. LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache)
  154. yield
  155. ensure
  156. LocalCacheRegistry.set_cache_for(local_cache_key, save_cache)
  157. end
  158. end
  159. end
  160. end
  161. end
  162. end

lib/active_support/cache/strategy/local_cache_middleware.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. # frozen_string_literal: true
  2. require "rack/body_proxy"
  3. require "rack/utils"
  4. module ActiveSupport
  5. module Cache
  6. module Strategy
  7. module LocalCache
  8. #--
  9. # This class wraps up local storage for middlewares. Only the middleware method should
  10. # construct them.
  11. class Middleware # :nodoc:
  12. attr_reader :name, :local_cache_key
  13. def initialize(name, local_cache_key)
  14. @name = name
  15. @local_cache_key = local_cache_key
  16. @app = nil
  17. end
  18. def new(app)
  19. @app = app
  20. self
  21. end
  22. def call(env)
  23. LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
  24. response = @app.call(env)
  25. response[2] = ::Rack::BodyProxy.new(response[2]) do
  26. LocalCacheRegistry.set_cache_for(local_cache_key, nil)
  27. end
  28. cleanup_on_body_close = true
  29. response
  30. rescue Rack::Utils::InvalidParameterError
  31. [400, {}, []]
  32. ensure
  33. LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
  34. cleanup_on_body_close
  35. end
  36. end
  37. end
  38. end
  39. end
  40. end

lib/active_support/callbacks.rb

54.36% lines covered

344 relevant lines. 187 lines covered and 157 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/concern"
  3. 23 require "active_support/descendants_tracker"
  4. 23 require "active_support/core_ext/array/extract_options"
  5. 23 require "active_support/core_ext/class/attribute"
  6. 23 require "active_support/core_ext/string/filters"
  7. 23 require "thread"
  8. 23 module ActiveSupport
  9. # Callbacks are code hooks that are run at key points in an object's life cycle.
  10. # The typical use case is to have a base class define a set of callbacks
  11. # relevant to the other functionality it supplies, so that subclasses can
  12. # install callbacks that enhance or modify the base functionality without
  13. # needing to override or redefine methods of the base class.
  14. #
  15. # Mixing in this module allows you to define the events in the object's
  16. # life cycle that will support callbacks (via +ClassMethods.define_callbacks+),
  17. # set the instance methods, procs, or callback objects to be called (via
  18. # +ClassMethods.set_callback+), and run the installed callbacks at the
  19. # appropriate times (via +run_callbacks+).
  20. #
  21. # By default callbacks are halted by throwing +:abort+.
  22. # See +ClassMethods.define_callbacks+ for details.
  23. #
  24. # Three kinds of callbacks are supported: before callbacks, run before a
  25. # certain event; after callbacks, run after the event; and around callbacks,
  26. # blocks that surround the event, triggering it when they yield. Callback code
  27. # can be contained in instance methods, procs or lambdas, or callback objects
  28. # that respond to certain predetermined methods. See +ClassMethods.set_callback+
  29. # for details.
  30. #
  31. # class Record
  32. # include ActiveSupport::Callbacks
  33. # define_callbacks :save
  34. #
  35. # def save
  36. # run_callbacks :save do
  37. # puts "- save"
  38. # end
  39. # end
  40. # end
  41. #
  42. # class PersonRecord < Record
  43. # set_callback :save, :before, :saving_message
  44. # def saving_message
  45. # puts "saving..."
  46. # end
  47. #
  48. # set_callback :save, :after do |object|
  49. # puts "saved"
  50. # end
  51. # end
  52. #
  53. # person = PersonRecord.new
  54. # person.save
  55. #
  56. # Output:
  57. # saving...
  58. # - save
  59. # saved
  60. 23 module Callbacks
  61. 23 extend Concern
  62. 23 included do
  63. 40 extend ActiveSupport::DescendantsTracker
  64. 40 class_attribute :__callbacks, instance_writer: false, default: {}
  65. end
  66. 23 CALLBACK_FILTER_TYPES = [:before, :after, :around]
  67. # Runs the callbacks for the given event.
  68. #
  69. # Calls the before and around callbacks in the order they were set, yields
  70. # the block (if given one), and then runs the after callbacks in reverse
  71. # order.
  72. #
  73. # If the callback chain was halted, returns +false+. Otherwise returns the
  74. # result of the block, +nil+ if no callbacks have been set, or +true+
  75. # if callbacks have been set but no block is given.
  76. #
  77. # run_callbacks :save do
  78. # save
  79. # end
  80. #
  81. #--
  82. #
  83. # As this method is used in many places, and often wraps large portions of
  84. # user code, it has an additional design goal of minimizing its impact on
  85. # the visible call stack. An exception from inside a :before or :after
  86. # callback can be as noisy as it likes -- but when control has passed
  87. # smoothly through and into the supplied block, we want as little evidence
  88. # as possible that we were here.
  89. 23 def run_callbacks(kind)
  90. callbacks = __callbacks[kind.to_sym]
  91. if callbacks.empty?
  92. yield if block_given?
  93. else
  94. env = Filters::Environment.new(self, false, nil)
  95. next_sequence = callbacks.compile
  96. # Common case: no 'around' callbacks defined
  97. if next_sequence.final?
  98. next_sequence.invoke_before(env)
  99. env.value = !env.halted && (!block_given? || yield)
  100. next_sequence.invoke_after(env)
  101. env.value
  102. else
  103. invoke_sequence = Proc.new do
  104. skipped = nil
  105. while true
  106. current = next_sequence
  107. current.invoke_before(env)
  108. if current.final?
  109. env.value = !env.halted && (!block_given? || yield)
  110. elsif current.skip?(env)
  111. (skipped ||= []) << current
  112. next_sequence = next_sequence.nested
  113. next
  114. else
  115. next_sequence = next_sequence.nested
  116. begin
  117. target, block, method, *arguments = current.expand_call_template(env, invoke_sequence)
  118. target.send(method, *arguments, &block)
  119. ensure
  120. next_sequence = current
  121. end
  122. end
  123. current.invoke_after(env)
  124. skipped.pop.invoke_after(env) while skipped&.first
  125. break env.value
  126. end
  127. end
  128. invoke_sequence.call
  129. end
  130. end
  131. end
  132. 23 private
  133. # A hook invoked every time a before callback is halted.
  134. # This can be overridden in ActiveSupport::Callbacks implementors in order
  135. # to provide better debugging/logging.
  136. 23 def halted_callback_hook(filter, name)
  137. end
  138. 23 module Conditionals # :nodoc:
  139. 23 class Value
  140. 23 def initialize(&block)
  141. @block = block
  142. end
  143. 23 def call(target, value); @block.call(value); end
  144. end
  145. end
  146. 23 module Filters
  147. 23 Environment = Struct.new(:target, :halted, :value)
  148. 23 class Before
  149. 23 def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter, name)
  150. halted_lambda = chain_config[:terminator]
  151. if user_conditions.any?
  152. halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
  153. else
  154. halting(callback_sequence, user_callback, halted_lambda, filter, name)
  155. end
  156. end
  157. 23 def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
  158. callback_sequence.before do |env|
  159. target = env.target
  160. value = env.value
  161. halted = env.halted
  162. if !halted && user_conditions.all? { |c| c.call(target, value) }
  163. result_lambda = -> { user_callback.call target, value }
  164. env.halted = halted_lambda.call(target, result_lambda)
  165. if env.halted
  166. target.send :halted_callback_hook, filter, name
  167. end
  168. end
  169. env
  170. end
  171. end
  172. 23 private_class_method :halting_and_conditional
  173. 23 def self.halting(callback_sequence, user_callback, halted_lambda, filter, name)
  174. callback_sequence.before do |env|
  175. target = env.target
  176. value = env.value
  177. halted = env.halted
  178. unless halted
  179. result_lambda = -> { user_callback.call target, value }
  180. env.halted = halted_lambda.call(target, result_lambda)
  181. if env.halted
  182. target.send :halted_callback_hook, filter, name
  183. end
  184. end
  185. env
  186. end
  187. end
  188. 23 private_class_method :halting
  189. end
  190. 23 class After
  191. 23 def self.build(callback_sequence, user_callback, user_conditions, chain_config)
  192. if chain_config[:skip_after_callbacks_if_terminated]
  193. if user_conditions.any?
  194. halting_and_conditional(callback_sequence, user_callback, user_conditions)
  195. else
  196. halting(callback_sequence, user_callback)
  197. end
  198. else
  199. if user_conditions.any?
  200. conditional callback_sequence, user_callback, user_conditions
  201. else
  202. simple callback_sequence, user_callback
  203. end
  204. end
  205. end
  206. 23 def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
  207. callback_sequence.after do |env|
  208. target = env.target
  209. value = env.value
  210. halted = env.halted
  211. if !halted && user_conditions.all? { |c| c.call(target, value) }
  212. user_callback.call target, value
  213. end
  214. env
  215. end
  216. end
  217. 23 private_class_method :halting_and_conditional
  218. 23 def self.halting(callback_sequence, user_callback)
  219. callback_sequence.after do |env|
  220. unless env.halted
  221. user_callback.call env.target, env.value
  222. end
  223. env
  224. end
  225. end
  226. 23 private_class_method :halting
  227. 23 def self.conditional(callback_sequence, user_callback, user_conditions)
  228. callback_sequence.after do |env|
  229. target = env.target
  230. value = env.value
  231. if user_conditions.all? { |c| c.call(target, value) }
  232. user_callback.call target, value
  233. end
  234. env
  235. end
  236. end
  237. 23 private_class_method :conditional
  238. 23 def self.simple(callback_sequence, user_callback)
  239. callback_sequence.after do |env|
  240. user_callback.call env.target, env.value
  241. env
  242. end
  243. end
  244. 23 private_class_method :simple
  245. end
  246. end
  247. 23 class Callback #:nodoc:#
  248. 23 def self.build(chain, filter, kind, options)
  249. 154 if filter.is_a?(String)
  250. raise ArgumentError, <<-MSG.squish
  251. Passing string to define a callback is not supported. See the `.set_callback`
  252. documentation to see supported values.
  253. MSG
  254. end
  255. 154 new chain.name, filter, kind, options, chain.config
  256. end
  257. 23 attr_accessor :kind, :name
  258. 23 attr_reader :chain_config
  259. 23 def initialize(name, filter, kind, options, chain_config)
  260. 154 @chain_config = chain_config
  261. 154 @name = name
  262. 154 @kind = kind
  263. 154 @filter = filter
  264. 154 @key = compute_identifier filter
  265. 154 @if = check_conditionals(options[:if])
  266. 154 @unless = check_conditionals(options[:unless])
  267. end
  268. 265 def filter; @key; end
  269. 23 def raw_filter; @filter; end
  270. 23 def merge_conditional_options(chain, if_option:, unless_option:)
  271. 13 options = {
  272. if: @if.dup,
  273. unless: @unless.dup
  274. }
  275. 13 options[:if].concat Array(unless_option)
  276. 13 options[:unless].concat Array(if_option)
  277. 13 self.class.build chain, @filter, @kind, options
  278. end
  279. 23 def matches?(_kind, _filter)
  280. 196 @kind == _kind && filter == _filter
  281. end
  282. 23 def duplicates?(other)
  283. 253 case @filter
  284. when Symbol
  285. 158 matches?(other.kind, other.filter)
  286. else
  287. 95 false
  288. end
  289. end
  290. # Wraps code with filter
  291. 23 def apply(callback_sequence)
  292. user_conditions = conditions_lambdas
  293. user_callback = CallTemplate.build(@filter, self)
  294. case kind
  295. when :before
  296. Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter, name)
  297. when :after
  298. Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
  299. when :around
  300. callback_sequence.around(user_callback, user_conditions)
  301. end
  302. end
  303. 23 def current_scopes
  304. Array(chain_config[:scope]).map { |s| public_send(s) }
  305. end
  306. 23 private
  307. 23 EMPTY_ARRAY = [].freeze
  308. 23 private_constant :EMPTY_ARRAY
  309. 23 def check_conditionals(conditionals)
  310. 308 return EMPTY_ARRAY if conditionals.blank?
  311. 51 conditionals = Array(conditionals)
  312. 109 if conditionals.any? { |c| c.is_a?(String) }
  313. raise ArgumentError, <<-MSG.squish
  314. Passing string to be evaluated in :if and :unless conditional
  315. options is not supported. Pass a symbol for an instance method,
  316. or a lambda, proc or block, instead.
  317. MSG
  318. end
  319. 51 conditionals.freeze
  320. end
  321. 23 def compute_identifier(filter)
  322. 154 case filter
  323. when ::Proc
  324. 58 filter.object_id
  325. else
  326. 96 filter
  327. end
  328. end
  329. 23 def conditions_lambdas
  330. @if.map { |c| CallTemplate.build(c, self).make_lambda } +
  331. @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
  332. end
  333. end
  334. # A future invocation of user-supplied code (either as a callback,
  335. # or a condition filter).
  336. 23 class CallTemplate # :nodoc:
  337. 23 def initialize(target, method, arguments, block)
  338. @override_target = target
  339. @method_name = method
  340. @arguments = arguments
  341. @override_block = block
  342. end
  343. # Return the parts needed to make this call, with the given
  344. # input values.
  345. #
  346. # Returns an array of the form:
  347. #
  348. # [target, block, method, *arguments]
  349. #
  350. # This array can be used as such:
  351. #
  352. # target.send(method, *arguments, &block)
  353. #
  354. # The actual invocation is left up to the caller to minimize
  355. # call stack pollution.
  356. 23 def expand(target, value, block)
  357. expanded = [@override_target || target, @override_block || block, @method_name]
  358. @arguments.each do |arg|
  359. case arg
  360. when :value then expanded << value
  361. when :target then expanded << target
  362. when :block then expanded << (block || raise(ArgumentError))
  363. end
  364. end
  365. expanded
  366. end
  367. # Return a lambda that will make this call when given the input
  368. # values.
  369. 23 def make_lambda
  370. lambda do |target, value, &block|
  371. target, block, method, *arguments = expand(target, value, block)
  372. target.send(method, *arguments, &block)
  373. end
  374. end
  375. # Return a lambda that will make this call when given the input
  376. # values, but then return the boolean inverse of that result.
  377. 23 def inverted_lambda
  378. lambda do |target, value, &block|
  379. target, block, method, *arguments = expand(target, value, block)
  380. ! target.send(method, *arguments, &block)
  381. end
  382. end
  383. # Filters support:
  384. #
  385. # Symbols:: A method to call.
  386. # Procs:: A proc to call with the object.
  387. # Objects:: An object with a <tt>before_foo</tt> method on it to call.
  388. #
  389. # All of these objects are converted into a CallTemplate and handled
  390. # the same after this point.
  391. 23 def self.build(filter, callback)
  392. case filter
  393. when Symbol
  394. new(nil, filter, [], nil)
  395. when Conditionals::Value
  396. new(filter, :call, [:target, :value], nil)
  397. when ::Proc
  398. if filter.arity > 1
  399. new(nil, :instance_exec, [:target, :block], filter)
  400. elsif filter.arity > 0
  401. new(nil, :instance_exec, [:target], filter)
  402. else
  403. new(nil, :instance_exec, [], filter)
  404. end
  405. else
  406. method_to_call = callback.current_scopes.join("_")
  407. new(filter, method_to_call, [:target], nil)
  408. end
  409. end
  410. end
  411. # Execute before and after filters in a sequence instead of
  412. # chaining them with nested lambda calls, see:
  413. # https://github.com/rails/rails/issues/18011
  414. 23 class CallbackSequence # :nodoc:
  415. 23 def initialize(nested = nil, call_template = nil, user_conditions = nil)
  416. @nested = nested
  417. @call_template = call_template
  418. @user_conditions = user_conditions
  419. @before = []
  420. @after = []
  421. end
  422. 23 def before(&before)
  423. @before.unshift(before)
  424. self
  425. end
  426. 23 def after(&after)
  427. @after.push(after)
  428. self
  429. end
  430. 23 def around(call_template, user_conditions)
  431. CallbackSequence.new(self, call_template, user_conditions)
  432. end
  433. 23 def skip?(arg)
  434. arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
  435. end
  436. 23 attr_reader :nested
  437. 23 def final?
  438. !@call_template
  439. end
  440. 23 def expand_call_template(arg, block)
  441. @call_template.expand(arg.target, arg.value, block)
  442. end
  443. 23 def invoke_before(arg)
  444. @before.each { |b| b.call(arg) }
  445. end
  446. 23 def invoke_after(arg)
  447. @after.each { |a| a.call(arg) }
  448. end
  449. end
  450. 23 class CallbackChain #:nodoc:#
  451. 23 include Enumerable
  452. 23 attr_reader :name, :config
  453. 23 def initialize(name, config)
  454. 66 @name = name
  455. 66 @config = {
  456. scope: [:kind],
  457. terminator: default_terminator
  458. }.merge!(config)
  459. 66 @chain = []
  460. 66 @callbacks = nil
  461. 66 @mutex = Mutex.new
  462. end
  463. 37 def each(&block); @chain.each(&block); end
  464. 36 def index(o); @chain.index(o); end
  465. 23 def empty?; @chain.empty?; end
  466. 23 def insert(index, o)
  467. 13 @callbacks = nil
  468. 13 @chain.insert(index, o)
  469. end
  470. 23 def delete(o)
  471. 14 @callbacks = nil
  472. 14 @chain.delete(o)
  473. end
  474. 23 def clear
  475. 1 @callbacks = nil
  476. 1 @chain.clear
  477. 1 self
  478. end
  479. 23 def initialize_copy(other)
  480. 145 @callbacks = nil
  481. 145 @chain = other.chain.dup
  482. 145 @mutex = Mutex.new
  483. end
  484. 23 def compile
  485. @callbacks || @mutex.synchronize do
  486. final_sequence = CallbackSequence.new
  487. @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
  488. callback.apply callback_sequence
  489. end
  490. end
  491. end
  492. 23 def append(*callbacks)
  493. 271 callbacks.each { |c| append_one(c) }
  494. end
  495. 23 def prepend(*callbacks)
  496. callbacks.each { |c| prepend_one(c) }
  497. end
  498. 23 protected
  499. 23 attr_reader :chain
  500. 23 private
  501. 23 def append_one(callback)
  502. 141 @callbacks = nil
  503. 141 remove_duplicates(callback)
  504. 141 @chain.push(callback)
  505. end
  506. 23 def prepend_one(callback)
  507. @callbacks = nil
  508. remove_duplicates(callback)
  509. @chain.unshift(callback)
  510. end
  511. 23 def remove_duplicates(callback)
  512. 141 @callbacks = nil
  513. 394 @chain.delete_if { |c| callback.duplicates?(c) }
  514. end
  515. 23 def default_terminator
  516. 66 Proc.new do |target, result_lambda|
  517. terminate = true
  518. catch(:abort) do
  519. result_lambda.call
  520. terminate = false
  521. end
  522. terminate
  523. end
  524. end
  525. end
  526. 23 module ClassMethods
  527. 23 def normalize_callback_params(filters, block) # :nodoc:
  528. 144 type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
  529. 144 options = filters.extract_options!
  530. 144 filters.unshift(block) if block
  531. 144 [type, filters, options.dup]
  532. end
  533. # This is used internally to append, prepend and skip callbacks to the
  534. # CallbackChain.
  535. 23 def __update_callbacks(name) #:nodoc:
  536. 144 ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
  537. 144 chain = target.get_callbacks name
  538. 144 yield target, chain.dup
  539. end
  540. end
  541. # Install a callback for the given event.
  542. #
  543. # set_callback :save, :before, :before_method
  544. # set_callback :save, :after, :after_method, if: :condition
  545. # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
  546. #
  547. # The second argument indicates whether the callback is to be run +:before+,
  548. # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
  549. # means the first example above can also be written as:
  550. #
  551. # set_callback :save, :before_method
  552. #
  553. # The callback can be specified as a symbol naming an instance method; as a
  554. # proc, lambda, or block; or as an object that responds to a certain method
  555. # determined by the <tt>:scope</tt> argument to +define_callbacks+.
  556. #
  557. # If a proc, lambda, or block is given, its body is evaluated in the context
  558. # of the current object. It can also optionally accept the current object as
  559. # an argument.
  560. #
  561. # Before and around callbacks are called in the order that they are set;
  562. # after callbacks are called in the reverse order.
  563. #
  564. # Around callbacks can access the return value from the event, if it
  565. # wasn't halted, from the +yield+ call.
  566. #
  567. # ===== Options
  568. #
  569. # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance
  570. # method or a proc; the callback will be called only when they all return
  571. # a true value.
  572. #
  573. # If a proc is given, its body is evaluated in the context of the
  574. # current object. It can also optionally accept the current object as
  575. # an argument.
  576. # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an
  577. # instance method or a proc; the callback will be called only when they
  578. # all return a false value.
  579. #
  580. # If a proc is given, its body is evaluated in the context of the
  581. # current object. It can also optionally accept the current object as
  582. # an argument.
  583. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
  584. # existing chain rather than appended.
  585. 23 def set_callback(name, *filter_list, &block)
  586. 130 type, filters, options = normalize_callback_params(filter_list, block)
  587. 130 self_chain = get_callbacks name
  588. 130 mapped = filters.map do |filter|
  589. 141 Callback.build(self_chain, filter, type, options)
  590. end
  591. 130 __update_callbacks(name) do |target, chain|
  592. 130 options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
  593. 130 target.set_callbacks name, chain
  594. end
  595. end
  596. # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
  597. # <tt>:unless</tt> options may be passed in order to control when the
  598. # callback is skipped.
  599. #
  600. # class Writer < Person
  601. # skip_callback :validate, :before, :check_membership, if: -> { age > 18 }
  602. # end
  603. #
  604. # An <tt>ArgumentError</tt> will be raised if the callback has not
  605. # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
  606. 23 def skip_callback(name, *filter_list, &block)
  607. 14 type, filters, options = normalize_callback_params(filter_list, block)
  608. 14 options[:raise] = true unless options.key?(:raise)
  609. 14 __update_callbacks(name) do |target, chain|
  610. 14 filters.each do |filter|
  611. 52 callback = chain.find { |c| c.matches?(type, filter) }
  612. 14 if !callback && options[:raise]
  613. raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
  614. end
  615. 14 if callback && (options.key?(:if) || options.key?(:unless))
  616. 13 new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
  617. 13 chain.insert(chain.index(callback), new_callback)
  618. end
  619. 14 chain.delete(callback)
  620. end
  621. 14 target.set_callbacks name, chain
  622. end
  623. end
  624. # Remove all set callbacks for the given event.
  625. 23 def reset_callbacks(name)
  626. 1 callbacks = get_callbacks name
  627. 1 ActiveSupport::DescendantsTracker.descendants(self).each do |target|
  628. chain = target.get_callbacks(name).dup
  629. callbacks.each { |c| chain.delete(c) }
  630. target.set_callbacks name, chain
  631. end
  632. 1 set_callbacks(name, callbacks.dup.clear)
  633. end
  634. # Define sets of events in the object life cycle that support callbacks.
  635. #
  636. # define_callbacks :validate
  637. # define_callbacks :initialize, :save, :destroy
  638. #
  639. # ===== Options
  640. #
  641. # * <tt>:terminator</tt> - Determines when a before filter will halt the
  642. # callback chain, preventing following before and around callbacks from
  643. # being called and the event from being triggered.
  644. # This should be a lambda to be executed.
  645. # The current object and the result lambda of the callback will be provided
  646. # to the terminator lambda.
  647. #
  648. # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false }
  649. #
  650. # In this example, if any before validate callbacks returns +false+,
  651. # any successive before and around callback is not executed.
  652. #
  653. # The default terminator halts the chain when a callback throws +:abort+.
  654. #
  655. # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
  656. # callbacks should be terminated by the <tt>:terminator</tt> option. By
  657. # default after callbacks are executed no matter if callback chain was
  658. # terminated or not. This option has no effect if <tt>:terminator</tt>
  659. # option is set to +nil+.
  660. #
  661. # * <tt>:scope</tt> - Indicates which methods should be executed when an
  662. # object is used as a callback.
  663. #
  664. # class Audit
  665. # def before(caller)
  666. # puts 'Audit: before is called'
  667. # end
  668. #
  669. # def before_save(caller)
  670. # puts 'Audit: before_save is called'
  671. # end
  672. # end
  673. #
  674. # class Account
  675. # include ActiveSupport::Callbacks
  676. #
  677. # define_callbacks :save
  678. # set_callback :save, :before, Audit.new
  679. #
  680. # def save
  681. # run_callbacks :save do
  682. # puts 'save in main'
  683. # end
  684. # end
  685. # end
  686. #
  687. # In the above case whenever you save an account the method
  688. # <tt>Audit#before</tt> will be called. On the other hand
  689. #
  690. # define_callbacks :save, scope: [:kind, :name]
  691. #
  692. # would trigger <tt>Audit#before_save</tt> instead. That's constructed
  693. # by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
  694. # case "kind" is "before" and "name" is "save". In this context +:kind+
  695. # and +:name+ have special meanings: +:kind+ refers to the kind of
  696. # callback (before/after/around) and +:name+ refers to the method on
  697. # which callbacks are being defined.
  698. #
  699. # A declaration like
  700. #
  701. # define_callbacks :save, scope: [:name]
  702. #
  703. # would call <tt>Audit#save</tt>.
  704. #
  705. # ===== Notes
  706. #
  707. # +names+ passed to +define_callbacks+ must not end with
  708. # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
  709. #
  710. # Calling +define_callbacks+ multiple times with the same +names+ will
  711. # overwrite previous callbacks registered with +set_callback+.
  712. 23 def define_callbacks(*names)
  713. 43 options = names.extract_options!
  714. 43 names.each do |name|
  715. 66 name = name.to_sym
  716. 66 ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
  717. 66 target.set_callbacks name, CallbackChain.new(name, options)
  718. end
  719. 66 module_eval <<-RUBY, __FILE__, __LINE__ + 1
  720. def _run_#{name}_callbacks(&block)
  721. run_callbacks #{name.inspect}, &block
  722. end
  723. def self._#{name}_callbacks
  724. get_callbacks(#{name.inspect})
  725. end
  726. def self._#{name}_callbacks=(value)
  727. set_callbacks(#{name.inspect}, value)
  728. end
  729. def _#{name}_callbacks
  730. __callbacks[#{name.inspect}]
  731. end
  732. RUBY
  733. end
  734. end
  735. 23 protected
  736. 23 def get_callbacks(name) # :nodoc:
  737. 275 __callbacks[name.to_sym]
  738. end
  739. 23 if Module.instance_method(:method_defined?).arity == 1 # Ruby 2.5 and older
  740. 23 def set_callbacks(name, callbacks) # :nodoc:
  741. 211 self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
  742. end
  743. else # Ruby 2.6 and newer
  744. def set_callbacks(name, callbacks) # :nodoc:
  745. unless singleton_class.method_defined?(:__callbacks, false)
  746. self.__callbacks = __callbacks.dup
  747. end
  748. self.__callbacks[name.to_sym] = callbacks
  749. self.__callbacks
  750. end
  751. end
  752. end
  753. end
  754. end

lib/active_support/concern.rb

82.22% lines covered

45 relevant lines. 37 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module ActiveSupport
  3. # A typical module looks like this:
  4. #
  5. # module M
  6. # def self.included(base)
  7. # base.extend ClassMethods
  8. # base.class_eval do
  9. # scope :disabled, -> { where(disabled: true) }
  10. # end
  11. # end
  12. #
  13. # module ClassMethods
  14. # ...
  15. # end
  16. # end
  17. #
  18. # By using <tt>ActiveSupport::Concern</tt> the above module could instead be
  19. # written as:
  20. #
  21. # require "active_support/concern"
  22. #
  23. # module M
  24. # extend ActiveSupport::Concern
  25. #
  26. # included do
  27. # scope :disabled, -> { where(disabled: true) }
  28. # end
  29. #
  30. # class_methods do
  31. # ...
  32. # end
  33. # end
  34. #
  35. # Moreover, it gracefully handles module dependencies. Given a +Foo+ module
  36. # and a +Bar+ module which depends on the former, we would typically write the
  37. # following:
  38. #
  39. # module Foo
  40. # def self.included(base)
  41. # base.class_eval do
  42. # def self.method_injected_by_foo
  43. # ...
  44. # end
  45. # end
  46. # end
  47. # end
  48. #
  49. # module Bar
  50. # def self.included(base)
  51. # base.method_injected_by_foo
  52. # end
  53. # end
  54. #
  55. # class Host
  56. # include Foo # We need to include this dependency for Bar
  57. # include Bar # Bar is the module that Host really needs
  58. # end
  59. #
  60. # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
  61. # could try to hide these from +Host+ directly including +Foo+ in +Bar+:
  62. #
  63. # module Bar
  64. # include Foo
  65. # def self.included(base)
  66. # base.method_injected_by_foo
  67. # end
  68. # end
  69. #
  70. # class Host
  71. # include Bar
  72. # end
  73. #
  74. # Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
  75. # is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
  76. # module dependencies are properly resolved:
  77. #
  78. # require "active_support/concern"
  79. #
  80. # module Foo
  81. # extend ActiveSupport::Concern
  82. # included do
  83. # def self.method_injected_by_foo
  84. # ...
  85. # end
  86. # end
  87. # end
  88. #
  89. # module Bar
  90. # extend ActiveSupport::Concern
  91. # include Foo
  92. #
  93. # included do
  94. # self.method_injected_by_foo
  95. # end
  96. # end
  97. #
  98. # class Host
  99. # include Bar # It works, now Bar takes care of its dependencies
  100. # end
  101. #
  102. # === Prepending concerns
  103. #
  104. # Just like `include`, concerns also support `prepend` with a corresponding
  105. # `prepended do` callback. `module ClassMethods` or `class_methods do` are
  106. # prepended as well.
  107. #
  108. # `prepend` is also used for any dependencies.
  109. 24 module Concern
  110. 24 class MultipleIncludedBlocks < StandardError #:nodoc:
  111. 24 def initialize
  112. super "Cannot define multiple 'included' blocks for a Concern"
  113. end
  114. end
  115. 24 class MultiplePrependBlocks < StandardError #:nodoc:
  116. 24 def initialize
  117. super "Cannot define multiple 'prepended' blocks for a Concern"
  118. end
  119. end
  120. 24 def self.extended(base) #:nodoc:
  121. 149 base.instance_variable_set(:@_dependencies, [])
  122. end
  123. 24 def append_features(base) #:nodoc:
  124. 124 if base.instance_variable_defined?(:@_dependencies)
  125. 3 base.instance_variable_get(:@_dependencies) << self
  126. 3 false
  127. else
  128. 121 return false if base < self
  129. 120 @_dependencies.each { |dep| base.include(dep) }
  130. 120 super
  131. 120 base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
  132. 120 base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
  133. end
  134. end
  135. 24 def prepend_features(base) #:nodoc:
  136. 1 if base.instance_variable_defined?(:@_dependencies)
  137. base.instance_variable_get(:@_dependencies).unshift self
  138. false
  139. else
  140. 1 return false if base < self
  141. 1 @_dependencies.each { |dep| base.prepend(dep) }
  142. 1 super
  143. 1 base.singleton_class.prepend const_get(:ClassMethods) if const_defined?(:ClassMethods)
  144. 1 base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
  145. end
  146. end
  147. # Evaluate given block in context of base class,
  148. # so that you can write class macros here.
  149. # When you define more than one +included+ block, it raises an exception.
  150. 24 def included(base = nil, &block)
  151. 245 if base.nil?
  152. 121 if instance_variable_defined?(:@_included_block)
  153. if @_included_block.source_location != block.source_location
  154. raise MultipleIncludedBlocks
  155. end
  156. else
  157. 121 @_included_block = block
  158. end
  159. else
  160. 124 super
  161. end
  162. end
  163. # Evaluate given block in context of base class,
  164. # so that you can write class macros here.
  165. # When you define more than one +prepended+ block, it raises an exception.
  166. 24 def prepended(base = nil, &block)
  167. 2 if base.nil?
  168. 1 if instance_variable_defined?(:@_prepended_block)
  169. if @_prepended_block.source_location != block.source_location
  170. raise MultiplePrependBlocks
  171. end
  172. else
  173. 1 @_prepended_block = block
  174. end
  175. else
  176. 1 super
  177. end
  178. end
  179. # Define class methods from given block.
  180. # You can define private class methods as well.
  181. #
  182. # module Example
  183. # extend ActiveSupport::Concern
  184. #
  185. # class_methods do
  186. # def foo; puts 'foo'; end
  187. #
  188. # private
  189. # def bar; puts 'bar'; end
  190. # end
  191. # end
  192. #
  193. # class Buzz
  194. # include Example
  195. # end
  196. #
  197. # Buzz.foo # => "foo"
  198. # Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)
  199. 24 def class_methods(&class_methods_module_definition)
  200. 5 mod = const_defined?(:ClassMethods, false) ?
  201. const_get(:ClassMethods) :
  202. const_set(:ClassMethods, Module.new)
  203. 5 mod.module_eval(&class_methods_module_definition)
  204. end
  205. end
  206. end

lib/active_support/concurrency/load_interlock_aware_monitor.rb

52.94% lines covered

17 relevant lines. 9 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "monitor"
  3. 1 module ActiveSupport
  4. 1 module Concurrency
  5. # A monitor that will permit dependency loading while blocked waiting for
  6. # the lock.
  7. 1 class LoadInterlockAwareMonitor < Monitor
  8. 1 EXCEPTION_NEVER = { Exception => :never }.freeze
  9. 1 EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
  10. 1 private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
  11. # Enters an exclusive section, but allows dependency loading while blocked
  12. 1 def mon_enter
  13. mon_try_enter ||
  14. ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
  15. end
  16. 1 def synchronize
  17. Thread.handle_interrupt(EXCEPTION_NEVER) do
  18. mon_enter
  19. begin
  20. Thread.handle_interrupt(EXCEPTION_IMMEDIATE) do
  21. yield
  22. end
  23. ensure
  24. mon_exit
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

lib/active_support/concurrency/share_lock.rb

27.84% lines covered

97 relevant lines. 27 lines covered and 70 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "thread"
  3. 3 require "monitor"
  4. 3 module ActiveSupport
  5. 3 module Concurrency
  6. # A share/exclusive lock, otherwise known as a read/write lock.
  7. #
  8. # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
  9. 3 class ShareLock
  10. 3 include MonitorMixin
  11. # We track Thread objects, instead of just using counters, because
  12. # we need exclusive locks to be reentrant, and we need to be able
  13. # to upgrade share locks to exclusive.
  14. 3 def raw_state # :nodoc:
  15. synchronize do
  16. threads = @sleeping.keys | @sharing.keys | @waiting.keys
  17. threads |= [@exclusive_thread] if @exclusive_thread
  18. data = {}
  19. threads.each do |thread|
  20. purpose, compatible = @waiting[thread]
  21. data[thread] = {
  22. thread: thread,
  23. sharing: @sharing[thread],
  24. exclusive: @exclusive_thread == thread,
  25. purpose: purpose,
  26. compatible: compatible,
  27. waiting: !!@waiting[thread],
  28. sleeper: @sleeping[thread],
  29. }
  30. end
  31. # NB: Yields while holding our *internal* synchronize lock,
  32. # which is supposed to be used only for a few instructions at
  33. # a time. This allows the caller to inspect additional state
  34. # without things changing out from underneath, but would have
  35. # disastrous effects upon normal operation. Fortunately, this
  36. # method is only intended to be called when things have
  37. # already gone wrong.
  38. yield data
  39. end
  40. end
  41. 3 def initialize
  42. 3 super()
  43. 3 @cv = new_cond
  44. 3 @sharing = Hash.new(0)
  45. 3 @waiting = {}
  46. 3 @sleeping = {}
  47. 3 @exclusive_thread = nil
  48. 3 @exclusive_depth = 0
  49. end
  50. # Returns false if +no_wait+ is set and the lock is not
  51. # immediately available. Otherwise, returns true after the lock
  52. # has been acquired.
  53. #
  54. # +purpose+ and +compatible+ work together; while this thread is
  55. # waiting for the exclusive lock, it will yield its share (if any)
  56. # to any other attempt whose +purpose+ appears in this attempt's
  57. # +compatible+ list. This allows a "loose" upgrade, which, being
  58. # less strict, prevents some classes of deadlocks.
  59. #
  60. # For many resources, loose upgrades are sufficient: if a thread
  61. # is awaiting a lock, it is not running any other code. With
  62. # +purpose+ matching, it is possible to yield only to other
  63. # threads whose activity will not interfere.
  64. 3 def start_exclusive(purpose: nil, compatible: [], no_wait: false)
  65. synchronize do
  66. unless @exclusive_thread == Thread.current
  67. if busy_for_exclusive?(purpose)
  68. return false if no_wait
  69. yield_shares(purpose: purpose, compatible: compatible, block_share: true) do
  70. wait_for(:start_exclusive) { busy_for_exclusive?(purpose) }
  71. end
  72. end
  73. @exclusive_thread = Thread.current
  74. end
  75. @exclusive_depth += 1
  76. true
  77. end
  78. end
  79. # Relinquish the exclusive lock. Must only be called by the thread
  80. # that called start_exclusive (and currently holds the lock).
  81. 3 def stop_exclusive(compatible: [])
  82. synchronize do
  83. raise "invalid unlock" if @exclusive_thread != Thread.current
  84. @exclusive_depth -= 1
  85. if @exclusive_depth == 0
  86. @exclusive_thread = nil
  87. if eligible_waiters?(compatible)
  88. yield_shares(compatible: compatible, block_share: true) do
  89. wait_for(:stop_exclusive) { @exclusive_thread || eligible_waiters?(compatible) }
  90. end
  91. end
  92. @cv.broadcast
  93. end
  94. end
  95. end
  96. 3 def start_sharing
  97. synchronize do
  98. if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current
  99. # We already hold a lock; nothing to wait for
  100. elsif @waiting[Thread.current]
  101. # We're nested inside a +yield_shares+ call: we'll resume as
  102. # soon as there isn't an exclusive lock in our way
  103. wait_for(:start_sharing) { @exclusive_thread }
  104. else
  105. # This is an initial / outermost share call: any outstanding
  106. # requests for an exclusive lock get to go first
  107. wait_for(:start_sharing) { busy_for_sharing?(false) }
  108. end
  109. @sharing[Thread.current] += 1
  110. end
  111. end
  112. 3 def stop_sharing
  113. synchronize do
  114. if @sharing[Thread.current] > 1
  115. @sharing[Thread.current] -= 1
  116. else
  117. @sharing.delete Thread.current
  118. @cv.broadcast
  119. end
  120. end
  121. end
  122. # Execute the supplied block while holding the Exclusive lock. If
  123. # +no_wait+ is set and the lock is not immediately available,
  124. # returns +nil+ without yielding. Otherwise, returns the result of
  125. # the block.
  126. #
  127. # See +start_exclusive+ for other options.
  128. 3 def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false)
  129. if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait)
  130. begin
  131. yield
  132. ensure
  133. stop_exclusive(compatible: after_compatible)
  134. end
  135. end
  136. end
  137. # Execute the supplied block while holding the Share lock.
  138. 3 def sharing
  139. start_sharing
  140. begin
  141. yield
  142. ensure
  143. stop_sharing
  144. end
  145. end
  146. # Temporarily give up all held Share locks while executing the
  147. # supplied block, allowing any +compatible+ exclusive lock request
  148. # to proceed.
  149. 3 def yield_shares(purpose: nil, compatible: [], block_share: false)
  150. loose_shares = previous_wait = nil
  151. synchronize do
  152. if loose_shares = @sharing.delete(Thread.current)
  153. if previous_wait = @waiting[Thread.current]
  154. purpose = nil unless purpose == previous_wait[0]
  155. compatible &= previous_wait[1]
  156. end
  157. compatible |= [false] unless block_share
  158. @waiting[Thread.current] = [purpose, compatible]
  159. end
  160. @cv.broadcast
  161. end
  162. begin
  163. yield
  164. ensure
  165. synchronize do
  166. wait_for(:yield_shares) { @exclusive_thread && @exclusive_thread != Thread.current }
  167. if previous_wait
  168. @waiting[Thread.current] = previous_wait
  169. else
  170. @waiting.delete Thread.current
  171. end
  172. @sharing[Thread.current] = loose_shares if loose_shares
  173. end
  174. end
  175. end
  176. 3 private
  177. # Must be called within synchronize
  178. 3 def busy_for_exclusive?(purpose)
  179. busy_for_sharing?(purpose) ||
  180. @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
  181. end
  182. 3 def busy_for_sharing?(purpose)
  183. (@exclusive_thread && @exclusive_thread != Thread.current) ||
  184. @waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) }
  185. end
  186. 3 def eligible_waiters?(compatible)
  187. @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } }
  188. end
  189. 3 def wait_for(method)
  190. @sleeping[Thread.current] = method
  191. @cv.wait_while { yield }
  192. ensure
  193. @sleeping.delete Thread.current
  194. end
  195. end
  196. end
  197. end

lib/active_support/configurable.rb

75.0% lines covered

32 relevant lines. 24 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/concern"
  3. 1 require "active_support/ordered_options"
  4. 1 module ActiveSupport
  5. # Configurable provides a <tt>config</tt> method to store and retrieve
  6. # configuration options as an <tt>OrderedOptions</tt>.
  7. 1 module Configurable
  8. 1 extend ActiveSupport::Concern
  9. 1 class Configuration < ActiveSupport::InheritableOptions
  10. 1 def compile_methods!
  11. self.class.compile_methods!(keys)
  12. end
  13. # Compiles reader methods so we don't have to go through method_missing.
  14. 1 def self.compile_methods!(keys)
  15. keys.reject { |m| method_defined?(m) }.each do |key|
  16. class_eval <<-RUBY, __FILE__, __LINE__ + 1
  17. def #{key}; _get(#{key.inspect}); end
  18. RUBY
  19. end
  20. end
  21. end
  22. 1 module ClassMethods
  23. 1 def config
  24. @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
  25. superclass.config.inheritable_copy
  26. else
  27. # create a new "anonymous" class that will host the compiled reader methods
  28. Class.new(Configuration).new
  29. end
  30. end
  31. 1 def configure
  32. yield config
  33. end
  34. # Allows you to add shortcut so that you don't have to refer to attribute
  35. # through config. Also look at the example for config to contrast.
  36. #
  37. # Defines both class and instance config accessors.
  38. #
  39. # class User
  40. # include ActiveSupport::Configurable
  41. # config_accessor :allowed_access
  42. # end
  43. #
  44. # User.allowed_access # => nil
  45. # User.allowed_access = false
  46. # User.allowed_access # => false
  47. #
  48. # user = User.new
  49. # user.allowed_access # => false
  50. # user.allowed_access = true
  51. # user.allowed_access # => true
  52. #
  53. # User.allowed_access # => false
  54. #
  55. # The attribute name must be a valid method name in Ruby.
  56. #
  57. # class User
  58. # include ActiveSupport::Configurable
  59. # config_accessor :"1_Badname"
  60. # end
  61. # # => NameError: invalid config attribute name
  62. #
  63. # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
  64. # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
  65. #
  66. # class User
  67. # include ActiveSupport::Configurable
  68. # config_accessor :allowed_access, instance_reader: false, instance_writer: false
  69. # end
  70. #
  71. # User.allowed_access = false
  72. # User.allowed_access # => false
  73. #
  74. # User.new.allowed_access = true # => NoMethodError
  75. # User.new.allowed_access # => NoMethodError
  76. #
  77. # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
  78. #
  79. # class User
  80. # include ActiveSupport::Configurable
  81. # config_accessor :allowed_access, instance_accessor: false
  82. # end
  83. #
  84. # User.allowed_access = false
  85. # User.allowed_access # => false
  86. #
  87. # User.new.allowed_access = true # => NoMethodError
  88. # User.new.allowed_access # => NoMethodError
  89. #
  90. # Also you can pass a block to set up the attribute with a default value.
  91. #
  92. # class User
  93. # include ActiveSupport::Configurable
  94. # config_accessor :hair_colors do
  95. # [:brown, :black, :blonde, :red]
  96. # end
  97. # end
  98. #
  99. # User.hair_colors # => [:brown, :black, :blonde, :red]
  100. 1 def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc:
  101. 3 names.each do |name|
  102. 3 raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name)
  103. 3 reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
  104. 3 writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
  105. 3 singleton_class.class_eval reader, __FILE__, reader_line
  106. 3 singleton_class.class_eval writer, __FILE__, writer_line
  107. 3 if instance_accessor
  108. 2 class_eval reader, __FILE__, reader_line if instance_reader
  109. 2 class_eval writer, __FILE__, writer_line if instance_writer
  110. end
  111. 3 send("#{name}=", yield) if block_given?
  112. end
  113. end
  114. 1 private :config_accessor
  115. end
  116. # Reads and writes attributes from a configuration <tt>OrderedOptions</tt>.
  117. #
  118. # require "active_support/configurable"
  119. #
  120. # class User
  121. # include ActiveSupport::Configurable
  122. # end
  123. #
  124. # user = User.new
  125. #
  126. # user.config.allowed_access = true
  127. # user.config.level = 1
  128. #
  129. # user.config.allowed_access # => true
  130. # user.config.level # => 1
  131. 1 def config
  132. @_config ||= self.class.config.inheritable_copy
  133. end
  134. end
  135. end

lib/active_support/configuration_file.rb

0.0% lines covered

33 relevant lines. 0 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. # Reads a YAML configuration file, evaluating any ERB, then
  4. # parsing the resulting YAML.
  5. #
  6. # Warns in case of YAML confusing characters, like invisible
  7. # non-breaking spaces.
  8. class ConfigurationFile # :nodoc:
  9. class FormatError < StandardError; end
  10. def initialize(content_path)
  11. @content_path = content_path.to_s
  12. @content = read content_path
  13. end
  14. def self.parse(content_path, **options)
  15. new(content_path).parse(**options)
  16. end
  17. def parse(context: nil, **options)
  18. YAML.load(render(context), **options) || {}
  19. rescue Psych::SyntaxError => error
  20. raise "YAML syntax error occurred while parsing #{@content_path}. " \
  21. "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
  22. "Error: #{error.message}"
  23. end
  24. private
  25. def read(content_path)
  26. require "yaml"
  27. require "erb"
  28. File.read(content_path).tap do |content|
  29. if content.include?("\u00A0")
  30. warn "File contains invisible non-breaking spaces, you may want to remove those"
  31. end
  32. end
  33. end
  34. def render(context)
  35. erb = ERB.new(@content).tap { |e| e.filename = @content_path }
  36. context ? erb.result(context) : erb.result
  37. end
  38. end
  39. end

lib/active_support/core_ext.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path|
  3. require path
  4. end

lib/active_support/core_ext/array.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/wrap"
  3. 1 require "active_support/core_ext/array/access"
  4. 1 require "active_support/core_ext/array/conversions"
  5. 1 require "active_support/core_ext/array/extract"
  6. 1 require "active_support/core_ext/array/extract_options"
  7. 1 require "active_support/core_ext/array/grouping"
  8. 1 require "active_support/core_ext/array/inquiry"

lib/active_support/core_ext/array/access.rb

48.15% lines covered

27 relevant lines. 13 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Array
  3. # Returns the tail of the array from +position+.
  4. #
  5. # %w( a b c d ).from(0) # => ["a", "b", "c", "d"]
  6. # %w( a b c d ).from(2) # => ["c", "d"]
  7. # %w( a b c d ).from(10) # => []
  8. # %w().from(0) # => []
  9. # %w( a b c d ).from(-2) # => ["c", "d"]
  10. # %w( a b c ).from(-10) # => []
  11. 1 def from(position)
  12. self[position, length] || []
  13. end
  14. # Returns the beginning of the array up to +position+.
  15. #
  16. # %w( a b c d ).to(0) # => ["a"]
  17. # %w( a b c d ).to(2) # => ["a", "b", "c"]
  18. # %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
  19. # %w().to(0) # => []
  20. # %w( a b c d ).to(-2) # => ["a", "b", "c"]
  21. # %w( a b c ).to(-10) # => []
  22. 1 def to(position)
  23. if position >= 0
  24. take position + 1
  25. else
  26. self[0..position]
  27. end
  28. end
  29. # Returns a new array that includes the passed elements.
  30. #
  31. # [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
  32. # [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]
  33. 1 def including(*elements)
  34. self + elements.flatten(1)
  35. end
  36. # Returns a copy of the Array excluding the specified elements.
  37. #
  38. # ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
  39. # [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ]
  40. #
  41. # Note: This is an optimization of <tt>Enumerable#excluding</tt> that uses <tt>Array#-</tt>
  42. # instead of <tt>Array#reject</tt> for performance reasons.
  43. 1 def excluding(*elements)
  44. self - elements.flatten(1)
  45. end
  46. # Alias for #excluding.
  47. 1 def without(*elements)
  48. excluding(*elements)
  49. end
  50. # Equal to <tt>self[1]</tt>.
  51. #
  52. # %w( a b c d e ).second # => "b"
  53. 1 def second
  54. self[1]
  55. end
  56. # Equal to <tt>self[2]</tt>.
  57. #
  58. # %w( a b c d e ).third # => "c"
  59. 1 def third
  60. self[2]
  61. end
  62. # Equal to <tt>self[3]</tt>.
  63. #
  64. # %w( a b c d e ).fourth # => "d"
  65. 1 def fourth
  66. self[3]
  67. end
  68. # Equal to <tt>self[4]</tt>.
  69. #
  70. # %w( a b c d e ).fifth # => "e"
  71. 1 def fifth
  72. self[4]
  73. end
  74. # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
  75. #
  76. # (1..42).to_a.forty_two # => 42
  77. 1 def forty_two
  78. self[41]
  79. end
  80. # Equal to <tt>self[-3]</tt>.
  81. #
  82. # %w( a b c d e ).third_to_last # => "c"
  83. 1 def third_to_last
  84. self[-3]
  85. end
  86. # Equal to <tt>self[-2]</tt>.
  87. #
  88. # %w( a b c d e ).second_to_last # => "d"
  89. 1 def second_to_last
  90. self[-2]
  91. end
  92. end

lib/active_support/core_ext/array/conversions.rb

23.91% lines covered

46 relevant lines. 11 lines covered and 35 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/xml_mini"
  3. 23 require "active_support/core_ext/hash/keys"
  4. 23 require "active_support/core_ext/string/inflections"
  5. 23 require "active_support/core_ext/object/to_param"
  6. 23 require "active_support/core_ext/object/to_query"
  7. 23 class Array
  8. # Converts the array to a comma-separated sentence where the last element is
  9. # joined by the connector word.
  10. #
  11. # You can pass the following options to change the default behavior. If you
  12. # pass an option key that doesn't exist in the list below, it will raise an
  13. # <tt>ArgumentError</tt>.
  14. #
  15. # ==== Options
  16. #
  17. # * <tt>:words_connector</tt> - The sign or word used to join the elements
  18. # in arrays with two or more elements (default: ", ").
  19. # * <tt>:two_words_connector</tt> - The sign or word used to join the elements
  20. # in arrays with two elements (default: " and ").
  21. # * <tt>:last_word_connector</tt> - The sign or word used to join the last element
  22. # in arrays with three or more elements (default: ", and ").
  23. # * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
  24. # the connector options defined on the 'support.array' namespace in the
  25. # corresponding dictionary file.
  26. #
  27. # ==== Examples
  28. #
  29. # [].to_sentence # => ""
  30. # ['one'].to_sentence # => "one"
  31. # ['one', 'two'].to_sentence # => "one and two"
  32. # ['one', 'two', 'three'].to_sentence # => "one, two, and three"
  33. #
  34. # ['one', 'two'].to_sentence(passing: 'invalid option')
  35. # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale
  36. #
  37. # ['one', 'two'].to_sentence(two_words_connector: '-')
  38. # # => "one-two"
  39. #
  40. # ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ')
  41. # # => "one or two or at least three"
  42. #
  43. # Using <tt>:locale</tt> option:
  44. #
  45. # # Given this locale dictionary:
  46. # #
  47. # # es:
  48. # # support:
  49. # # array:
  50. # # words_connector: " o "
  51. # # two_words_connector: " y "
  52. # # last_word_connector: " o al menos "
  53. #
  54. # ['uno', 'dos'].to_sentence(locale: :es)
  55. # # => "uno y dos"
  56. #
  57. # ['uno', 'dos', 'tres'].to_sentence(locale: :es)
  58. # # => "uno o dos o al menos tres"
  59. 23 def to_sentence(options = {})
  60. options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
  61. default_connectors = {
  62. words_connector: ", ",
  63. two_words_connector: " and ",
  64. last_word_connector: ", and "
  65. }
  66. if defined?(I18n)
  67. i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
  68. default_connectors.merge!(i18n_connectors)
  69. end
  70. options = default_connectors.merge!(options)
  71. case length
  72. when 0
  73. +""
  74. when 1
  75. +"#{self[0]}"
  76. when 2
  77. +"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
  78. else
  79. +"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
  80. end
  81. end
  82. # Extends <tt>Array#to_s</tt> to convert a collection of elements into a
  83. # comma separated id list if <tt>:db</tt> argument is given as the format.
  84. #
  85. # Blog.all.to_formatted_s(:db) # => "1,2,3"
  86. # Blog.none.to_formatted_s(:db) # => "null"
  87. # [1,2].to_formatted_s # => "[1, 2]"
  88. 23 def to_formatted_s(format = :default)
  89. case format
  90. when :db
  91. if empty?
  92. "null"
  93. else
  94. collect(&:id).join(",")
  95. end
  96. else
  97. to_default_s
  98. end
  99. end
  100. 23 alias_method :to_default_s, :to_s
  101. 23 alias_method :to_s, :to_formatted_s
  102. # Returns a string that represents the array in XML by invoking +to_xml+
  103. # on each element. Active Record collections delegate their representation
  104. # in XML to this method.
  105. #
  106. # All elements are expected to respond to +to_xml+, if any of them does
  107. # not then an exception is raised.
  108. #
  109. # The root node reflects the class name of the first element in plural
  110. # if all elements belong to the same type and that's not Hash:
  111. #
  112. # customer.projects.to_xml
  113. #
  114. # <?xml version="1.0" encoding="UTF-8"?>
  115. # <projects type="array">
  116. # <project>
  117. # <amount type="decimal">20000.0</amount>
  118. # <customer-id type="integer">1567</customer-id>
  119. # <deal-date type="date">2008-04-09</deal-date>
  120. # ...
  121. # </project>
  122. # <project>
  123. # <amount type="decimal">57230.0</amount>
  124. # <customer-id type="integer">1567</customer-id>
  125. # <deal-date type="date">2008-04-15</deal-date>
  126. # ...
  127. # </project>
  128. # </projects>
  129. #
  130. # Otherwise the root element is "objects":
  131. #
  132. # [{ foo: 1, bar: 2}, { baz: 3}].to_xml
  133. #
  134. # <?xml version="1.0" encoding="UTF-8"?>
  135. # <objects type="array">
  136. # <object>
  137. # <bar type="integer">2</bar>
  138. # <foo type="integer">1</foo>
  139. # </object>
  140. # <object>
  141. # <baz type="integer">3</baz>
  142. # </object>
  143. # </objects>
  144. #
  145. # If the collection is empty the root element is "nil-classes" by default:
  146. #
  147. # [].to_xml
  148. #
  149. # <?xml version="1.0" encoding="UTF-8"?>
  150. # <nil-classes type="array"/>
  151. #
  152. # To ensure a meaningful root element use the <tt>:root</tt> option:
  153. #
  154. # customer_with_no_projects.projects.to_xml(root: 'projects')
  155. #
  156. # <?xml version="1.0" encoding="UTF-8"?>
  157. # <projects type="array"/>
  158. #
  159. # By default name of the node for the children of root is <tt>root.singularize</tt>.
  160. # You can change it with the <tt>:children</tt> option.
  161. #
  162. # The +options+ hash is passed downwards:
  163. #
  164. # Message.all.to_xml(skip_types: true)
  165. #
  166. # <?xml version="1.0" encoding="UTF-8"?>
  167. # <messages>
  168. # <message>
  169. # <created-at>2008-03-07T09:58:18+01:00</created-at>
  170. # <id>1</id>
  171. # <name>1</name>
  172. # <updated-at>2008-03-07T09:58:18+01:00</updated-at>
  173. # <user-id>1</user-id>
  174. # </message>
  175. # </messages>
  176. #
  177. 23 def to_xml(options = {})
  178. require "active_support/builder" unless defined?(Builder::XmlMarkup)
  179. options = options.dup
  180. options[:indent] ||= 2
  181. options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
  182. options[:root] ||= \
  183. if first.class != Hash && all? { |e| e.is_a?(first.class) }
  184. underscored = ActiveSupport::Inflector.underscore(first.class.name)
  185. ActiveSupport::Inflector.pluralize(underscored).tr("/", "_")
  186. else
  187. "objects"
  188. end
  189. builder = options[:builder]
  190. builder.instruct! unless options.delete(:skip_instruct)
  191. root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
  192. children = options.delete(:children) || root.singularize
  193. attributes = options[:skip_types] ? {} : { type: "array" }
  194. if empty?
  195. builder.tag!(root, attributes)
  196. else
  197. builder.tag!(root, attributes) do
  198. each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
  199. yield builder if block_given?
  200. end
  201. end
  202. end
  203. end

lib/active_support/core_ext/array/extract.rb

28.57% lines covered

7 relevant lines. 2 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Array
  3. # Removes and returns the elements for which the block returns a true value.
  4. # If no block is given, an Enumerator is returned instead.
  5. #
  6. # numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  7. # odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
  8. # numbers # => [0, 2, 4, 6, 8]
  9. 1 def extract!
  10. return to_enum(:extract!) { size } unless block_given?
  11. extracted_elements = []
  12. reject! do |element|
  13. extracted_elements << element if yield(element)
  14. end
  15. extracted_elements
  16. end
  17. end

lib/active_support/core_ext/array/extract_options.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 class Hash
  3. # By default, only instances of Hash itself are extractable.
  4. # Subclasses of Hash may implement this method and return
  5. # true to declare themselves as extractable. If a Hash
  6. # is extractable, Array#extract_options! pops it from
  7. # the Array when it is the last element of the Array.
  8. 24 def extractable_options?
  9. 45 instance_of?(Hash)
  10. end
  11. end
  12. 24 class Array
  13. # Extracts options from a set of arguments. Removes and returns the last
  14. # element in the array if it's a hash, otherwise returns a blank hash.
  15. #
  16. # def options(*args)
  17. # args.extract_options!
  18. # end
  19. #
  20. # options(1, 2) # => {}
  21. # options(1, 2, a: :b) # => {:a=>:b}
  22. 24 def extract_options!
  23. 191 if last.is_a?(Hash) && last.extractable_options?
  24. 45 pop
  25. else
  26. 146 {}
  27. end
  28. end
  29. end

lib/active_support/core_ext/array/grouping.rb

11.43% lines covered

35 relevant lines. 4 lines covered and 31 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Array
  3. # Splits or iterates over the array in groups of size +number+,
  4. # padding any remaining slots with +fill_with+ unless it is +false+.
  5. #
  6. # %w(1 2 3 4 5 6 7 8 9 10).in_groups_of(3) {|group| p group}
  7. # ["1", "2", "3"]
  8. # ["4", "5", "6"]
  9. # ["7", "8", "9"]
  10. # ["10", nil, nil]
  11. #
  12. # %w(1 2 3 4 5).in_groups_of(2, '&nbsp;') {|group| p group}
  13. # ["1", "2"]
  14. # ["3", "4"]
  15. # ["5", "&nbsp;"]
  16. #
  17. # %w(1 2 3 4 5).in_groups_of(2, false) {|group| p group}
  18. # ["1", "2"]
  19. # ["3", "4"]
  20. # ["5"]
  21. 1 def in_groups_of(number, fill_with = nil)
  22. if number.to_i <= 0
  23. raise ArgumentError,
  24. "Group size must be a positive integer, was #{number.inspect}"
  25. end
  26. if fill_with == false
  27. collection = self
  28. else
  29. # size % number gives how many extra we have;
  30. # subtracting from number gives how many to add;
  31. # modulo number ensures we don't add group of just fill.
  32. padding = (number - size % number) % number
  33. collection = dup.concat(Array.new(padding, fill_with))
  34. end
  35. if block_given?
  36. collection.each_slice(number) { |slice| yield(slice) }
  37. else
  38. collection.each_slice(number).to_a
  39. end
  40. end
  41. # Splits or iterates over the array in +number+ of groups, padding any
  42. # remaining slots with +fill_with+ unless it is +false+.
  43. #
  44. # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
  45. # ["1", "2", "3", "4"]
  46. # ["5", "6", "7", nil]
  47. # ["8", "9", "10", nil]
  48. #
  49. # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3, '&nbsp;') {|group| p group}
  50. # ["1", "2", "3", "4"]
  51. # ["5", "6", "7", "&nbsp;"]
  52. # ["8", "9", "10", "&nbsp;"]
  53. #
  54. # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
  55. # ["1", "2", "3"]
  56. # ["4", "5"]
  57. # ["6", "7"]
  58. 1 def in_groups(number, fill_with = nil)
  59. # size.div number gives minor group size;
  60. # size % number gives how many objects need extra accommodation;
  61. # each group hold either division or division + 1 items.
  62. division = size.div number
  63. modulo = size % number
  64. # create a new array avoiding dup
  65. groups = []
  66. start = 0
  67. number.times do |index|
  68. length = division + (modulo > 0 && modulo > index ? 1 : 0)
  69. groups << last_group = slice(start, length)
  70. last_group << fill_with if fill_with != false &&
  71. modulo > 0 && length == division
  72. start += length
  73. end
  74. if block_given?
  75. groups.each { |g| yield(g) }
  76. else
  77. groups
  78. end
  79. end
  80. # Divides the array into one or more subarrays based on a delimiting +value+
  81. # or the result of an optional block.
  82. #
  83. # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
  84. # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
  85. 1 def split(value = nil)
  86. arr = dup
  87. result = []
  88. if block_given?
  89. while (idx = arr.index { |i| yield i })
  90. result << arr.shift(idx)
  91. arr.shift
  92. end
  93. else
  94. while (idx = arr.index(value))
  95. result << arr.shift(idx)
  96. arr.shift
  97. end
  98. end
  99. result << arr
  100. end
  101. end

lib/active_support/core_ext/array/inquiry.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/array_inquirer"
  3. 1 class Array
  4. # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
  5. # to check its string-like contents.
  6. #
  7. # pets = [:cat, :dog].inquiry
  8. #
  9. # pets.cat? # => true
  10. # pets.ferret? # => false
  11. #
  12. # pets.any?(:cat, :ferret) # => true
  13. # pets.any?(:ferret, :alligator) # => false
  14. 1 def inquiry
  15. ActiveSupport::ArrayInquirer.new(self)
  16. end
  17. end

lib/active_support/core_ext/array/prepend_and_append.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation"
  3. ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Array#append and Array#prepend natively, so requiring active_support/core_ext/array/prepend_and_append is no longer necessary. Requiring it will raise LoadError in Rails 6.1."

lib/active_support/core_ext/array/wrap.rb

28.57% lines covered

7 relevant lines. 2 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 13 class Array
  3. # Wraps its argument in an array unless it is already an array (or array-like).
  4. #
  5. # Specifically:
  6. #
  7. # * If the argument is +nil+ an empty array is returned.
  8. # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
  9. # * Otherwise, returns an array with the argument as its single element.
  10. #
  11. # Array.wrap(nil) # => []
  12. # Array.wrap([1, 2, 3]) # => [1, 2, 3]
  13. # Array.wrap(0) # => [0]
  14. #
  15. # This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
  16. #
  17. # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
  18. # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
  19. # an array with the argument as its single element right away.
  20. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
  21. # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
  22. # * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+
  23. # it returns an array with the argument as its single element.
  24. #
  25. # The last point is easily explained with some enumerables:
  26. #
  27. # Array(foo: :bar) # => [[:foo, :bar]]
  28. # Array.wrap(foo: :bar) # => [{:foo=>:bar}]
  29. #
  30. # There's also a related idiom that uses the splat operator:
  31. #
  32. # [*object]
  33. #
  34. # which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise.
  35. #
  36. # The differences with <tt>Kernel#Array</tt> explained above
  37. # apply to the rest of <tt>object</tt>s.
  38. 13 def self.wrap(object)
  39. if object.nil?
  40. []
  41. elsif object.respond_to?(:to_ary)
  42. object.to_ary || [object]
  43. else
  44. [object]
  45. end
  46. end
  47. end

lib/active_support/core_ext/benchmark.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "benchmark"
  3. 2 class << Benchmark
  4. # Benchmark realtime in milliseconds.
  5. #
  6. # Benchmark.realtime { User.all }
  7. # # => 8.0e-05
  8. #
  9. # Benchmark.ms { User.all }
  10. # # => 0.074
  11. 2 def ms(&block)
  12. 1000 * realtime(&block)
  13. end
  14. end

lib/active_support/core_ext/big_decimal.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/big_decimal/conversions"

lib/active_support/core_ext/big_decimal/conversions.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "bigdecimal"
  3. 2 require "bigdecimal/util"
  4. 2 module ActiveSupport
  5. 2 module BigDecimalWithDefaultFormat #:nodoc:
  6. 2 def to_s(format = "F")
  7. 1 super(format)
  8. end
  9. end
  10. end
  11. 2 BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat)

lib/active_support/core_ext/class.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/class/attribute"
  3. 1 require "active_support/core_ext/class/subclasses"

lib/active_support/core_ext/class/attribute.rb

94.44% lines covered

18 relevant lines. 17 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/module/redefine_method"
  3. 23 class Class
  4. # Declare a class-level attribute whose value is inheritable by subclasses.
  5. # Subclasses can change their own value and it will not impact parent class.
  6. #
  7. # ==== Options
  8. #
  9. # * <tt>:instance_reader</tt> - Sets the instance reader method (defaults to true).
  10. # * <tt>:instance_writer</tt> - Sets the instance writer method (defaults to true).
  11. # * <tt>:instance_accessor</tt> - Sets both instance methods (defaults to true).
  12. # * <tt>:instance_predicate</tt> - Sets a predicate method (defaults to true).
  13. # * <tt>:default</tt> - Sets a default value for the attribute (defaults to nil).
  14. #
  15. # ==== Examples
  16. #
  17. # class Base
  18. # class_attribute :setting
  19. # end
  20. #
  21. # class Subclass < Base
  22. # end
  23. #
  24. # Base.setting = true
  25. # Subclass.setting # => true
  26. # Subclass.setting = false
  27. # Subclass.setting # => false
  28. # Base.setting # => true
  29. #
  30. # In the above case as long as Subclass does not assign a value to setting
  31. # by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
  32. # would read value assigned to parent class. Once Subclass assigns a value then
  33. # the value assigned by Subclass would be returned.
  34. #
  35. # This matches normal Ruby method inheritance: think of writing an attribute
  36. # on a subclass as overriding the reader method. However, you need to be aware
  37. # when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
  38. # In such cases, you don't want to do changes in place. Instead use setters:
  39. #
  40. # Base.setting = []
  41. # Base.setting # => []
  42. # Subclass.setting # => []
  43. #
  44. # # Appending in child changes both parent and child because it is the same object:
  45. # Subclass.setting << :foo
  46. # Base.setting # => [:foo]
  47. # Subclass.setting # => [:foo]
  48. #
  49. # # Use setters to not propagate changes:
  50. # Base.setting = []
  51. # Subclass.setting += [:foo]
  52. # Base.setting # => []
  53. # Subclass.setting # => [:foo]
  54. #
  55. # For convenience, an instance predicate method is defined as well.
  56. # To skip it, pass <tt>instance_predicate: false</tt>.
  57. #
  58. # Subclass.setting? # => false
  59. #
  60. # Instances may overwrite the class value in the same way:
  61. #
  62. # Base.setting = true
  63. # object = Base.new
  64. # object.setting # => true
  65. # object.setting = false
  66. # object.setting # => false
  67. # Base.setting # => true
  68. #
  69. # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
  70. #
  71. # object.setting # => NoMethodError
  72. # object.setting? # => NoMethodError
  73. #
  74. # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
  75. #
  76. # object.setting = false # => NoMethodError
  77. #
  78. # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
  79. #
  80. # To set a default value for the attribute, pass <tt>default:</tt>, like so:
  81. #
  82. # class_attribute :settings, default: {}
  83. 23 def class_attribute(*attrs, instance_accessor: true,
  84. instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
  85. 113 class_methods, methods = [], []
  86. 113 attrs.each do |name|
  87. 113 unless name.is_a?(Symbol) || name.is_a?(String)
  88. raise TypeError, "#{name.inspect} is not a symbol nor a string"
  89. end
  90. 113 class_methods << <<~RUBY # In case the method exists and is not public
  91. silence_redefinition_of_method def #{name}
  92. end
  93. RUBY
  94. 113 methods << <<~RUBY if instance_reader
  95. silence_redefinition_of_method def #{name}
  96. defined?(@#{name}) ? @#{name} : self.class.#{name}
  97. end
  98. RUBY
  99. 113 class_methods << <<~RUBY
  100. silence_redefinition_of_method def #{name}=(value)
  101. redefine_method(:#{name}) { value } if singleton_class?
  102. redefine_singleton_method(:#{name}) { value }
  103. value
  104. end
  105. RUBY
  106. 113 methods << <<~RUBY if instance_writer
  107. silence_redefinition_of_method(:#{name}=)
  108. attr_writer :#{name}
  109. RUBY
  110. 113 if instance_predicate
  111. 113 class_methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
  112. 113 if instance_reader
  113. 113 methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
  114. end
  115. end
  116. end
  117. 113 location = caller_locations(1, 1).first
  118. 113 class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
  119. 226 attrs.each { |name| public_send("#{name}=", default) }
  120. end
  121. end

lib/active_support/core_ext/class/attribute_accessors.rb

0.0% lines covered

1 relevant lines. 0 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. # cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
  3. # but we keep this around for libraries that directly require it knowing they
  4. # want cattr_*. No need to deprecate.
  5. require "active_support/core_ext/module/attribute_accessors"

lib/active_support/core_ext/class/subclasses.rb

50.0% lines covered

6 relevant lines. 3 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Class
  3. # Returns an array with all classes that are < than its receiver.
  4. #
  5. # class C; end
  6. # C.descendants # => []
  7. #
  8. # class B < C; end
  9. # C.descendants # => [B]
  10. #
  11. # class A < B; end
  12. # C.descendants # => [B, A]
  13. #
  14. # class D < C; end
  15. # C.descendants # => [B, A, D]
  16. 1 def descendants
  17. ObjectSpace.each_object(singleton_class).reject do |k|
  18. k.singleton_class? || k == self
  19. end
  20. end
  21. # Returns an array with the direct children of +self+.
  22. #
  23. # class Foo; end
  24. # class Bar < Foo; end
  25. # class Baz < Bar; end
  26. #
  27. # Foo.subclasses # => [Bar]
  28. 1 def subclasses
  29. descendants.select { |descendant| descendant.superclass == self }
  30. end
  31. end

lib/active_support/core_ext/date.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/date/acts_like"
  3. 2 require "active_support/core_ext/date/blank"
  4. 2 require "active_support/core_ext/date/calculations"
  5. 2 require "active_support/core_ext/date/conversions"
  6. 2 require "active_support/core_ext/date/zones"

lib/active_support/core_ext/date/acts_like.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 14 require "active_support/core_ext/object/acts_like"
  3. 14 class Date
  4. # Duck-types as a Date-like class. See Object#acts_like?.
  5. 14 def acts_like_date?
  6. true
  7. end
  8. end

lib/active_support/core_ext/date/blank.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "date"
  3. 2 class Date #:nodoc:
  4. # No Date is blank:
  5. #
  6. # Date.today.blank? # => false
  7. #
  8. # @return [false]
  9. 2 def blank?
  10. false
  11. end
  12. end

lib/active_support/core_ext/date/calculations.rb

60.0% lines covered

70 relevant lines. 42 lines covered and 28 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "date"
  3. 23 require "active_support/duration"
  4. 23 require "active_support/core_ext/object/acts_like"
  5. 23 require "active_support/core_ext/date/zones"
  6. 23 require "active_support/core_ext/time/zones"
  7. 23 require "active_support/core_ext/date_and_time/calculations"
  8. 23 class Date
  9. 23 include DateAndTime::Calculations
  10. 23 class << self
  11. 23 attr_accessor :beginning_of_week_default
  12. # Returns the week start (e.g. :monday) for the current request, if this has been set (via Date.beginning_of_week=).
  13. # If <tt>Date.beginning_of_week</tt> has not been set for the current request, returns the week start specified in <tt>config.beginning_of_week</tt>.
  14. # If no config.beginning_of_week was specified, returns :monday.
  15. 23 def beginning_of_week
  16. Thread.current[:beginning_of_week] || beginning_of_week_default || :monday
  17. end
  18. # Sets <tt>Date.beginning_of_week</tt> to a week start (e.g. :monday) for current request/thread.
  19. #
  20. # This method accepts any of the following day symbols:
  21. # :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
  22. 23 def beginning_of_week=(week_start)
  23. Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
  24. end
  25. # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.
  26. 23 def find_beginning_of_week!(week_start)
  27. raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start)
  28. week_start
  29. end
  30. # Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
  31. 23 def yesterday
  32. ::Date.current.yesterday
  33. end
  34. # Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
  35. 23 def tomorrow
  36. ::Date.current.tomorrow
  37. end
  38. # Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
  39. 23 def current
  40. ::Time.zone ? ::Time.zone.today : ::Date.today
  41. end
  42. end
  43. # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  44. # and then subtracts the specified number of seconds.
  45. 23 def ago(seconds)
  46. in_time_zone.since(-seconds)
  47. end
  48. # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  49. # and then adds the specified number of seconds
  50. 23 def since(seconds)
  51. in_time_zone.since(seconds)
  52. end
  53. 23 alias :in :since
  54. # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  55. 23 def beginning_of_day
  56. in_time_zone
  57. end
  58. 23 alias :midnight :beginning_of_day
  59. 23 alias :at_midnight :beginning_of_day
  60. 23 alias :at_beginning_of_day :beginning_of_day
  61. # Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00)
  62. 23 def middle_of_day
  63. in_time_zone.middle_of_day
  64. end
  65. 23 alias :midday :middle_of_day
  66. 23 alias :noon :middle_of_day
  67. 23 alias :at_midday :middle_of_day
  68. 23 alias :at_noon :middle_of_day
  69. 23 alias :at_middle_of_day :middle_of_day
  70. # Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
  71. 23 def end_of_day
  72. in_time_zone.end_of_day
  73. end
  74. 23 alias :at_end_of_day :end_of_day
  75. 23 def plus_with_duration(other) #:nodoc:
  76. if ActiveSupport::Duration === other
  77. other.since(self)
  78. else
  79. plus_without_duration(other)
  80. end
  81. end
  82. 23 alias_method :plus_without_duration, :+
  83. 23 alias_method :+, :plus_with_duration
  84. 23 def minus_with_duration(other) #:nodoc:
  85. if ActiveSupport::Duration === other
  86. plus_with_duration(-other)
  87. else
  88. minus_without_duration(other)
  89. end
  90. end
  91. 23 alias_method :minus_without_duration, :-
  92. 23 alias_method :-, :minus_with_duration
  93. # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
  94. # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
  95. 23 def advance(options)
  96. d = self
  97. d = d >> options[:years] * 12 if options[:years]
  98. d = d >> options[:months] if options[:months]
  99. d = d + options[:weeks] * 7 if options[:weeks]
  100. d = d + options[:days] if options[:days]
  101. d
  102. end
  103. # Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
  104. # The +options+ parameter is a hash with a combination of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>.
  105. #
  106. # Date.new(2007, 5, 12).change(day: 1) # => Date.new(2007, 5, 1)
  107. # Date.new(2007, 5, 12).change(year: 2005, month: 1) # => Date.new(2005, 1, 12)
  108. 23 def change(options)
  109. ::Date.new(
  110. options.fetch(:year, year),
  111. options.fetch(:month, month),
  112. options.fetch(:day, day)
  113. )
  114. end
  115. # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
  116. 23 def compare_with_coercion(other)
  117. if other.is_a?(Time)
  118. to_datetime <=> other
  119. else
  120. compare_without_coercion(other)
  121. end
  122. end
  123. 23 alias_method :compare_without_coercion, :<=>
  124. 23 alias_method :<=>, :compare_with_coercion
  125. end

lib/active_support/core_ext/date/conversions.rb

57.14% lines covered

28 relevant lines. 16 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "date"
  3. 2 require "active_support/inflector/methods"
  4. 2 require "active_support/core_ext/date/zones"
  5. 2 require "active_support/core_ext/module/redefine_method"
  6. 2 class Date
  7. 2 DATE_FORMATS = {
  8. short: "%d %b",
  9. long: "%B %d, %Y",
  10. db: "%Y-%m-%d",
  11. inspect: "%Y-%m-%d",
  12. number: "%Y%m%d",
  13. long_ordinal: lambda { |date|
  14. day_format = ActiveSupport::Inflector.ordinalize(date.day)
  15. date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
  16. },
  17. rfc822: "%d %b %Y",
  18. iso8601: lambda { |date| date.iso8601 }
  19. }
  20. # Convert to a formatted string. See DATE_FORMATS for predefined formats.
  21. #
  22. # This method is aliased to <tt>to_s</tt>.
  23. #
  24. # date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
  25. #
  26. # date.to_formatted_s(:db) # => "2007-11-10"
  27. # date.to_s(:db) # => "2007-11-10"
  28. #
  29. # date.to_formatted_s(:short) # => "10 Nov"
  30. # date.to_formatted_s(:number) # => "20071110"
  31. # date.to_formatted_s(:long) # => "November 10, 2007"
  32. # date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
  33. # date.to_formatted_s(:rfc822) # => "10 Nov 2007"
  34. # date.to_formatted_s(:iso8601) # => "2007-11-10"
  35. #
  36. # == Adding your own date formats to to_formatted_s
  37. # You can add your own formats to the Date::DATE_FORMATS hash.
  38. # Use the format name as the hash key and either a strftime string
  39. # or Proc instance that takes a date argument as the value.
  40. #
  41. # # config/initializers/date_formats.rb
  42. # Date::DATE_FORMATS[:month_and_year] = '%B %Y'
  43. # Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
  44. 2 def to_formatted_s(format = :default)
  45. if formatter = DATE_FORMATS[format]
  46. if formatter.respond_to?(:call)
  47. formatter.call(self).to_s
  48. else
  49. strftime(formatter)
  50. end
  51. else
  52. to_default_s
  53. end
  54. end
  55. 2 alias_method :to_default_s, :to_s
  56. 2 alias_method :to_s, :to_formatted_s
  57. # Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
  58. 2 def readable_inspect
  59. strftime("%a, %d %b %Y")
  60. end
  61. 2 alias_method :default_inspect, :inspect
  62. 2 alias_method :inspect, :readable_inspect
  63. 2 silence_redefinition_of_method :to_time
  64. # Converts a Date instance to a Time, where the time is set to the beginning of the day.
  65. # The timezone can be either :local or :utc (default :local).
  66. #
  67. # date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
  68. #
  69. # date.to_time # => 2007-11-10 00:00:00 0800
  70. # date.to_time(:local) # => 2007-11-10 00:00:00 0800
  71. #
  72. # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC
  73. #
  74. # NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ'].
  75. # If the *application's* timezone is needed, then use +in_time_zone+ instead.
  76. 2 def to_time(form = :local)
  77. raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form)
  78. ::Time.send(form, year, month, day)
  79. end
  80. 2 silence_redefinition_of_method :xmlschema
  81. # Returns a string which represents the time in used time zone as DateTime
  82. # defined by XML Schema:
  83. #
  84. # date = Date.new(2015, 05, 23) # => Sat, 23 May 2015
  85. # date.xmlschema # => "2015-05-23T00:00:00+04:00"
  86. 2 def xmlschema
  87. in_time_zone.xmlschema
  88. end
  89. end

lib/active_support/core_ext/date/zones.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "date"
  3. 23 require "active_support/core_ext/date_and_time/zones"
  4. 23 class Date
  5. 23 include DateAndTime::Zones
  6. end

lib/active_support/core_ext/date_and_time/calculations.rb

51.88% lines covered

133 relevant lines. 69 lines covered and 64 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/object/try"
  3. 23 require "active_support/core_ext/date_time/conversions"
  4. 23 module DateAndTime
  5. 23 module Calculations
  6. 23 DAYS_INTO_WEEK = {
  7. sunday: 0,
  8. monday: 1,
  9. tuesday: 2,
  10. wednesday: 3,
  11. thursday: 4,
  12. friday: 5,
  13. saturday: 6
  14. }
  15. 23 WEEKEND_DAYS = [ 6, 0 ]
  16. # Returns a new date/time representing yesterday.
  17. 23 def yesterday
  18. advance(days: -1)
  19. end
  20. # Returns a new date/time representing tomorrow.
  21. 23 def tomorrow
  22. advance(days: 1)
  23. end
  24. # Returns true if the date/time is today.
  25. 23 def today?
  26. to_date == ::Date.current
  27. end
  28. # Returns true if the date/time is tomorrow.
  29. 23 def tomorrow?
  30. to_date == ::Date.current.tomorrow
  31. end
  32. 23 alias :next_day? :tomorrow?
  33. # Returns true if the date/time is yesterday.
  34. 23 def yesterday?
  35. to_date == ::Date.current.yesterday
  36. end
  37. 23 alias :prev_day? :yesterday?
  38. # Returns true if the date/time is in the past.
  39. 23 def past?
  40. self < self.class.current
  41. end
  42. # Returns true if the date/time is in the future.
  43. 23 def future?
  44. self > self.class.current
  45. end
  46. # Returns true if the date/time falls on a Saturday or Sunday.
  47. 23 def on_weekend?
  48. WEEKEND_DAYS.include?(wday)
  49. end
  50. # Returns true if the date/time does not fall on a Saturday or Sunday.
  51. 23 def on_weekday?
  52. !WEEKEND_DAYS.include?(wday)
  53. end
  54. # Returns true if the date/time falls before <tt>date_or_time</tt>.
  55. 23 def before?(date_or_time)
  56. self < date_or_time
  57. end
  58. # Returns true if the date/time falls after <tt>date_or_time</tt>.
  59. 23 def after?(date_or_time)
  60. self > date_or_time
  61. end
  62. # Returns a new date/time the specified number of days ago.
  63. 23 def days_ago(days)
  64. advance(days: -days)
  65. end
  66. # Returns a new date/time the specified number of days in the future.
  67. 23 def days_since(days)
  68. advance(days: days)
  69. end
  70. # Returns a new date/time the specified number of weeks ago.
  71. 23 def weeks_ago(weeks)
  72. advance(weeks: -weeks)
  73. end
  74. # Returns a new date/time the specified number of weeks in the future.
  75. 23 def weeks_since(weeks)
  76. advance(weeks: weeks)
  77. end
  78. # Returns a new date/time the specified number of months ago.
  79. 23 def months_ago(months)
  80. advance(months: -months)
  81. end
  82. # Returns a new date/time the specified number of months in the future.
  83. 23 def months_since(months)
  84. advance(months: months)
  85. end
  86. # Returns a new date/time the specified number of years ago.
  87. 23 def years_ago(years)
  88. advance(years: -years)
  89. end
  90. # Returns a new date/time the specified number of years in the future.
  91. 23 def years_since(years)
  92. advance(years: years)
  93. end
  94. # Returns a new date/time at the start of the month.
  95. #
  96. # today = Date.today # => Thu, 18 Jun 2015
  97. # today.beginning_of_month # => Mon, 01 Jun 2015
  98. #
  99. # +DateTime+ objects will have a time set to 0:00.
  100. #
  101. # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000
  102. # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000
  103. 23 def beginning_of_month
  104. first_hour(change(day: 1))
  105. end
  106. 23 alias :at_beginning_of_month :beginning_of_month
  107. # Returns a new date/time at the start of the quarter.
  108. #
  109. # today = Date.today # => Fri, 10 Jul 2015
  110. # today.beginning_of_quarter # => Wed, 01 Jul 2015
  111. #
  112. # +DateTime+ objects will have a time set to 0:00.
  113. #
  114. # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
  115. # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000
  116. 23 def beginning_of_quarter
  117. first_quarter_month = month - (2 + month) % 3
  118. beginning_of_month.change(month: first_quarter_month)
  119. end
  120. 23 alias :at_beginning_of_quarter :beginning_of_quarter
  121. # Returns a new date/time at the end of the quarter.
  122. #
  123. # today = Date.today # => Fri, 10 Jul 2015
  124. # today.end_of_quarter # => Wed, 30 Sep 2015
  125. #
  126. # +DateTime+ objects will have a time set to 23:59:59.
  127. #
  128. # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
  129. # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000
  130. 23 def end_of_quarter
  131. last_quarter_month = month + (12 - month) % 3
  132. beginning_of_month.change(month: last_quarter_month).end_of_month
  133. end
  134. 23 alias :at_end_of_quarter :end_of_quarter
  135. # Returns a new date/time at the beginning of the year.
  136. #
  137. # today = Date.today # => Fri, 10 Jul 2015
  138. # today.beginning_of_year # => Thu, 01 Jan 2015
  139. #
  140. # +DateTime+ objects will have a time set to 0:00.
  141. #
  142. # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
  143. # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000
  144. 23 def beginning_of_year
  145. change(month: 1).beginning_of_month
  146. end
  147. 23 alias :at_beginning_of_year :beginning_of_year
  148. # Returns a new date/time representing the given day in the next week.
  149. #
  150. # today = Date.today # => Thu, 07 May 2015
  151. # today.next_week # => Mon, 11 May 2015
  152. #
  153. # The +given_day_in_next_week+ defaults to the beginning of the week
  154. # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
  155. # when set.
  156. #
  157. # today = Date.today # => Thu, 07 May 2015
  158. # today.next_week(:friday) # => Fri, 15 May 2015
  159. #
  160. # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true.
  161. #
  162. # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000
  163. # now.next_week # => Mon, 11 May 2015 00:00:00 +0000
  164. 23 def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false)
  165. result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
  166. same_time ? copy_time_to(result) : result
  167. end
  168. # Returns a new date/time representing the next weekday.
  169. 23 def next_weekday
  170. if next_day.on_weekend?
  171. next_week(:monday, same_time: true)
  172. else
  173. next_day
  174. end
  175. end
  176. # Short-hand for months_since(3)
  177. 23 def next_quarter
  178. months_since(3)
  179. end
  180. # Returns a new date/time representing the given day in the previous week.
  181. # Week is assumed to start on +start_day+, default is
  182. # +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
  183. # DateTime objects have their time set to 0:00 unless +same_time+ is true.
  184. 23 def prev_week(start_day = Date.beginning_of_week, same_time: false)
  185. result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
  186. same_time ? copy_time_to(result) : result
  187. end
  188. 23 alias_method :last_week, :prev_week
  189. # Returns a new date/time representing the previous weekday.
  190. 23 def prev_weekday
  191. if prev_day.on_weekend?
  192. copy_time_to(beginning_of_week(:friday))
  193. else
  194. prev_day
  195. end
  196. end
  197. 23 alias_method :last_weekday, :prev_weekday
  198. # Short-hand for months_ago(1).
  199. 23 def last_month
  200. months_ago(1)
  201. end
  202. # Short-hand for months_ago(3).
  203. 23 def prev_quarter
  204. months_ago(3)
  205. end
  206. 23 alias_method :last_quarter, :prev_quarter
  207. # Short-hand for years_ago(1).
  208. 23 def last_year
  209. years_ago(1)
  210. end
  211. # Returns the number of days to the start of the week on the given day.
  212. # Week is assumed to start on +start_day+, default is
  213. # +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
  214. 23 def days_to_week_start(start_day = Date.beginning_of_week)
  215. start_day_number = DAYS_INTO_WEEK.fetch(start_day)
  216. (wday - start_day_number) % 7
  217. end
  218. # Returns a new date/time representing the start of this week on the given day.
  219. # Week is assumed to start on +start_day+, default is
  220. # +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
  221. # +DateTime+ objects have their time set to 0:00.
  222. 23 def beginning_of_week(start_day = Date.beginning_of_week)
  223. result = days_ago(days_to_week_start(start_day))
  224. acts_like?(:time) ? result.midnight : result
  225. end
  226. 23 alias :at_beginning_of_week :beginning_of_week
  227. # Returns Monday of this week assuming that week starts on Monday.
  228. # +DateTime+ objects have their time set to 0:00.
  229. 23 def monday
  230. beginning_of_week(:monday)
  231. end
  232. # Returns a new date/time representing the end of this week on the given day.
  233. # Week is assumed to start on +start_day+, default is
  234. # +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
  235. # DateTime objects have their time set to 23:59:59.
  236. 23 def end_of_week(start_day = Date.beginning_of_week)
  237. last_hour(days_since(6 - days_to_week_start(start_day)))
  238. end
  239. 23 alias :at_end_of_week :end_of_week
  240. # Returns Sunday of this week assuming that week starts on Monday.
  241. # +DateTime+ objects have their time set to 23:59:59.
  242. 23 def sunday
  243. end_of_week(:monday)
  244. end
  245. # Returns a new date/time representing the end of the month.
  246. # DateTime objects will have a time set to 23:59:59.
  247. 23 def end_of_month
  248. last_day = ::Time.days_in_month(month, year)
  249. last_hour(days_since(last_day - day))
  250. end
  251. 23 alias :at_end_of_month :end_of_month
  252. # Returns a new date/time representing the end of the year.
  253. # DateTime objects will have a time set to 23:59:59.
  254. 23 def end_of_year
  255. change(month: 12).end_of_month
  256. end
  257. 23 alias :at_end_of_year :end_of_year
  258. # Returns a Range representing the whole day of the current date/time.
  259. 23 def all_day
  260. beginning_of_day..end_of_day
  261. end
  262. # Returns a Range representing the whole week of the current date/time.
  263. # Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set.
  264. 23 def all_week(start_day = Date.beginning_of_week)
  265. beginning_of_week(start_day)..end_of_week(start_day)
  266. end
  267. # Returns a Range representing the whole month of the current date/time.
  268. 23 def all_month
  269. beginning_of_month..end_of_month
  270. end
  271. # Returns a Range representing the whole quarter of the current date/time.
  272. 23 def all_quarter
  273. beginning_of_quarter..end_of_quarter
  274. end
  275. # Returns a Range representing the whole year of the current date/time.
  276. 23 def all_year
  277. beginning_of_year..end_of_year
  278. end
  279. # Returns a new date/time representing the next occurrence of the specified day of week.
  280. #
  281. # today = Date.today # => Thu, 14 Dec 2017
  282. # today.next_occurring(:monday) # => Mon, 18 Dec 2017
  283. # today.next_occurring(:thursday) # => Thu, 21 Dec 2017
  284. 23 def next_occurring(day_of_week)
  285. from_now = DAYS_INTO_WEEK.fetch(day_of_week) - wday
  286. from_now += 7 unless from_now > 0
  287. advance(days: from_now)
  288. end
  289. # Returns a new date/time representing the previous occurrence of the specified day of week.
  290. #
  291. # today = Date.today # => Thu, 14 Dec 2017
  292. # today.prev_occurring(:monday) # => Mon, 11 Dec 2017
  293. # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
  294. 23 def prev_occurring(day_of_week)
  295. ago = wday - DAYS_INTO_WEEK.fetch(day_of_week)
  296. ago += 7 unless ago > 0
  297. advance(days: -ago)
  298. end
  299. 23 private
  300. 23 def first_hour(date_or_time)
  301. date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
  302. end
  303. 23 def last_hour(date_or_time)
  304. date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
  305. end
  306. 23 def days_span(day)
  307. (DAYS_INTO_WEEK.fetch(day) - DAYS_INTO_WEEK.fetch(Date.beginning_of_week)) % 7
  308. end
  309. 23 def copy_time_to(other)
  310. other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec))
  311. end
  312. end
  313. end

lib/active_support/core_ext/date_and_time/compatibility.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/core_ext/module/attribute_accessors"
  3. 24 module DateAndTime
  4. 24 module Compatibility
  5. # If true, +to_time+ preserves the timezone offset of receiver.
  6. #
  7. # NOTE: With Ruby 2.4+ the default for +to_time+ changed from
  8. # converting to the local system time, to preserving the offset
  9. # of the receiver. For backwards compatibility we're overriding
  10. # this behavior, but new apps will have an initializer that sets
  11. # this to true, because the new behavior is preferred.
  12. 24 mattr_accessor :preserve_timezone, instance_writer: false, default: false
  13. # Change the output of <tt>ActiveSupport::TimeZone.utc_to_local</tt>.
  14. #
  15. # When `true`, it returns local times with an UTC offset, with `false` local
  16. # times are returned as UTC.
  17. #
  18. # # Given this zone:
  19. # zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
  20. #
  21. # # With `utc_to_local_returns_utc_offset_times = false`, local time is converted to UTC:
  22. # zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 UTC
  23. #
  24. # # With `utc_to_local_returns_utc_offset_times = true`, local time is returned with UTC offset:
  25. # zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 -0500
  26. 24 mattr_accessor :utc_to_local_returns_utc_offset_times, instance_writer: false, default: false
  27. end
  28. end

lib/active_support/core_ext/date_and_time/zones.rb

38.46% lines covered

13 relevant lines. 5 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module DateAndTime
  3. 23 module Zones
  4. # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
  5. # if Time.zone_default is set. Otherwise, it returns the current time.
  6. #
  7. # Time.zone = 'Hawaii' # => 'Hawaii'
  8. # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  9. # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
  10. #
  11. # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
  12. # instead of the operating system's time zone.
  13. #
  14. # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
  15. # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
  16. #
  17. # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
  18. # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
  19. 23 def in_time_zone(zone = ::Time.zone)
  20. time_zone = ::Time.find_zone! zone
  21. time = acts_like?(:time) ? self : nil
  22. if time_zone
  23. time_with_zone(time, time_zone)
  24. else
  25. time || to_time
  26. end
  27. end
  28. 23 private
  29. 23 def time_with_zone(time, zone)
  30. if time
  31. ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
  32. else
  33. ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
  34. end
  35. end
  36. end
  37. end

lib/active_support/core_ext/date_time.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/date_time/acts_like"
  3. 2 require "active_support/core_ext/date_time/blank"
  4. 2 require "active_support/core_ext/date_time/calculations"
  5. 2 require "active_support/core_ext/date_time/compatibility"
  6. 2 require "active_support/core_ext/date_time/conversions"

lib/active_support/core_ext/date_time/acts_like.rb

71.43% lines covered

7 relevant lines. 5 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "date"
  3. 2 require "active_support/core_ext/object/acts_like"
  4. 2 class DateTime
  5. # Duck-types as a Date-like class. See Object#acts_like?.
  6. 2 def acts_like_date?
  7. true
  8. end
  9. # Duck-types as a Time-like class. See Object#acts_like?.
  10. 2 def acts_like_time?
  11. true
  12. end
  13. end

lib/active_support/core_ext/date_time/blank.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "date"
  3. 2 class DateTime #:nodoc:
  4. # No DateTime is ever blank:
  5. #
  6. # DateTime.now.blank? # => false
  7. #
  8. # @return [false]
  9. 2 def blank?
  10. false
  11. end
  12. end

lib/active_support/core_ext/date_time/calculations.rb

48.81% lines covered

84 relevant lines. 41 lines covered and 43 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "date"
  3. 23 class DateTime
  4. 23 class << self
  5. # Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
  6. # <tt>config.time_zone</tt> are set, otherwise returns
  7. # <tt>Time.now.to_datetime</tt>.
  8. 23 def current
  9. ::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
  10. end
  11. end
  12. # Returns the number of seconds since 00:00:00.
  13. #
  14. # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0
  15. # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
  16. # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
  17. 23 def seconds_since_midnight
  18. sec + (min * 60) + (hour * 3600)
  19. end
  20. # Returns the number of seconds until 23:59:59.
  21. #
  22. # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
  23. # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
  24. # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
  25. 23 def seconds_until_end_of_day
  26. end_of_day.to_i - to_i
  27. end
  28. # Returns the fraction of a second as a +Rational+
  29. #
  30. # DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2)
  31. 23 def subsec
  32. sec_fraction
  33. end
  34. # Returns a new DateTime where one or more of the elements have been changed
  35. # according to the +options+ parameter. The time options (<tt>:hour</tt>,
  36. # <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
  37. # passed, then minute and sec is set to 0. If the hour and minute is passed,
  38. # then sec is set to 0. The +options+ parameter takes a hash with any of these
  39. # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>,
  40. # <tt>:min</tt>, <tt>:sec</tt>, <tt>:offset</tt>, <tt>:start</tt>.
  41. #
  42. # DateTime.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => DateTime.new(2012, 8, 1, 22, 35, 0)
  43. # DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => DateTime.new(1981, 8, 1, 22, 35, 0)
  44. # DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
  45. 23 def change(options)
  46. if new_nsec = options[:nsec]
  47. raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
  48. new_fraction = Rational(new_nsec, 1000000000)
  49. else
  50. new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
  51. new_fraction = Rational(new_usec, 1000000)
  52. end
  53. raise ArgumentError, "argument out of range" if new_fraction >= 1
  54. ::DateTime.civil(
  55. options.fetch(:year, year),
  56. options.fetch(:month, month),
  57. options.fetch(:day, day),
  58. options.fetch(:hour, hour),
  59. options.fetch(:min, options[:hour] ? 0 : min),
  60. options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
  61. options.fetch(:offset, offset),
  62. options.fetch(:start, start)
  63. )
  64. end
  65. # Uses Date to provide precise Time calculations for years, months, and days.
  66. # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
  67. # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
  68. # <tt>:minutes</tt>, <tt>:seconds</tt>.
  69. 23 def advance(options)
  70. unless options[:weeks].nil?
  71. options[:weeks], partial_weeks = options[:weeks].divmod(1)
  72. options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
  73. end
  74. unless options[:days].nil?
  75. options[:days], partial_days = options[:days].divmod(1)
  76. options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
  77. end
  78. d = to_date.advance(options)
  79. datetime_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
  80. seconds_to_advance = \
  81. options.fetch(:seconds, 0) +
  82. options.fetch(:minutes, 0) * 60 +
  83. options.fetch(:hours, 0) * 3600
  84. if seconds_to_advance.zero?
  85. datetime_advanced_by_date
  86. else
  87. datetime_advanced_by_date.since(seconds_to_advance)
  88. end
  89. end
  90. # Returns a new DateTime representing the time a number of seconds ago.
  91. # Do not use this method in combination with x.months, use months_ago instead!
  92. 23 def ago(seconds)
  93. since(-seconds)
  94. end
  95. # Returns a new DateTime representing the time a number of seconds since the
  96. # instance time. Do not use this method in combination with x.months, use
  97. # months_since instead!
  98. 23 def since(seconds)
  99. self + Rational(seconds, 86400)
  100. end
  101. 23 alias :in :since
  102. # Returns a new DateTime representing the start of the day (0:00).
  103. 23 def beginning_of_day
  104. change(hour: 0)
  105. end
  106. 23 alias :midnight :beginning_of_day
  107. 23 alias :at_midnight :beginning_of_day
  108. 23 alias :at_beginning_of_day :beginning_of_day
  109. # Returns a new DateTime representing the middle of the day (12:00)
  110. 23 def middle_of_day
  111. change(hour: 12)
  112. end
  113. 23 alias :midday :middle_of_day
  114. 23 alias :noon :middle_of_day
  115. 23 alias :at_midday :middle_of_day
  116. 23 alias :at_noon :middle_of_day
  117. 23 alias :at_middle_of_day :middle_of_day
  118. # Returns a new DateTime representing the end of the day (23:59:59).
  119. 23 def end_of_day
  120. change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
  121. end
  122. 23 alias :at_end_of_day :end_of_day
  123. # Returns a new DateTime representing the start of the hour (hh:00:00).
  124. 23 def beginning_of_hour
  125. change(min: 0)
  126. end
  127. 23 alias :at_beginning_of_hour :beginning_of_hour
  128. # Returns a new DateTime representing the end of the hour (hh:59:59).
  129. 23 def end_of_hour
  130. change(min: 59, sec: 59, usec: Rational(999999999, 1000))
  131. end
  132. 23 alias :at_end_of_hour :end_of_hour
  133. # Returns a new DateTime representing the start of the minute (hh:mm:00).
  134. 23 def beginning_of_minute
  135. change(sec: 0)
  136. end
  137. 23 alias :at_beginning_of_minute :beginning_of_minute
  138. # Returns a new DateTime representing the end of the minute (hh:mm:59).
  139. 23 def end_of_minute
  140. change(sec: 59, usec: Rational(999999999, 1000))
  141. end
  142. 23 alias :at_end_of_minute :end_of_minute
  143. # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
  144. 23 def localtime(utc_offset = nil)
  145. utc = new_offset(0)
  146. Time.utc(
  147. utc.year, utc.month, utc.day,
  148. utc.hour, utc.min, utc.sec + utc.sec_fraction
  149. ).getlocal(utc_offset)
  150. end
  151. 23 alias_method :getlocal, :localtime
  152. # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
  153. #
  154. # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
  155. # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC
  156. 23 def utc
  157. utc = new_offset(0)
  158. Time.utc(
  159. utc.year, utc.month, utc.day,
  160. utc.hour, utc.min, utc.sec + utc.sec_fraction
  161. )
  162. end
  163. 23 alias_method :getgm, :utc
  164. 23 alias_method :getutc, :utc
  165. 23 alias_method :gmtime, :utc
  166. # Returns +true+ if <tt>offset == 0</tt>.
  167. 23 def utc?
  168. offset == 0
  169. end
  170. # Returns the offset value in seconds.
  171. 23 def utc_offset
  172. (offset * 86400).to_i
  173. end
  174. # Layers additional behavior on DateTime#<=> so that Time and
  175. # ActiveSupport::TimeWithZone instances can be compared with a DateTime.
  176. 23 def <=>(other)
  177. if other.respond_to? :to_datetime
  178. super other.to_datetime rescue nil
  179. else
  180. super
  181. end
  182. end
  183. end

lib/active_support/core_ext/date_time/compatibility.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/date_and_time/compatibility"
  3. 2 require "active_support/core_ext/module/redefine_method"
  4. 2 class DateTime
  5. 2 include DateAndTime::Compatibility
  6. 2 silence_redefinition_of_method :to_time
  7. # Either return an instance of +Time+ with the same UTC offset
  8. # as +self+ or an instance of +Time+ representing the same time
  9. # in the local system timezone depending on the setting of
  10. # on the setting of +ActiveSupport.to_time_preserves_timezone+.
  11. 2 def to_time
  12. preserve_timezone ? getlocal(utc_offset) : getlocal
  13. end
  14. end

lib/active_support/core_ext/date_time/conversions.rb

58.33% lines covered

36 relevant lines. 21 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "date"
  3. 23 require "active_support/inflector/methods"
  4. 23 require "active_support/core_ext/time/conversions"
  5. 23 require "active_support/core_ext/date_time/calculations"
  6. 23 require "active_support/values/time_zone"
  7. 23 class DateTime
  8. # Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
  9. #
  10. # This method is aliased to <tt>to_s</tt>.
  11. #
  12. # === Examples
  13. # datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
  14. #
  15. # datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
  16. # datetime.to_s(:db) # => "2007-12-04 00:00:00"
  17. # datetime.to_s(:number) # => "20071204000000"
  18. # datetime.to_formatted_s(:short) # => "04 Dec 00:00"
  19. # datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
  20. # datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
  21. # datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
  22. # datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00"
  23. #
  24. # == Adding your own datetime formats to to_formatted_s
  25. # DateTime formats are shared with Time. You can add your own to the
  26. # Time::DATE_FORMATS hash. Use the format name as the hash key and
  27. # either a strftime string or Proc instance that takes a time or
  28. # datetime argument as the value.
  29. #
  30. # # config/initializers/time_formats.rb
  31. # Time::DATE_FORMATS[:month_and_year] = '%B %Y'
  32. # Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
  33. 23 def to_formatted_s(format = :default)
  34. if formatter = ::Time::DATE_FORMATS[format]
  35. formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  36. else
  37. to_default_s
  38. end
  39. end
  40. 23 alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s)
  41. 23 alias_method :to_s, :to_formatted_s
  42. # Returns a formatted string of the offset from UTC, or an alternative
  43. # string if the time zone is already UTC.
  44. #
  45. # datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
  46. # datetime.formatted_offset # => "-06:00"
  47. # datetime.formatted_offset(false) # => "-0600"
  48. 23 def formatted_offset(colon = true, alternate_utc_string = nil)
  49. utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
  50. end
  51. # Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005 14:30:00 +0000".
  52. 23 def readable_inspect
  53. to_s(:rfc822)
  54. end
  55. 23 alias_method :default_inspect, :inspect
  56. 23 alias_method :inspect, :readable_inspect
  57. # Returns DateTime with local offset for given year if format is local else
  58. # offset is zero.
  59. #
  60. # DateTime.civil_from_format :local, 2012
  61. # # => Sun, 01 Jan 2012 00:00:00 +0300
  62. # DateTime.civil_from_format :local, 2012, 12, 17
  63. # # => Mon, 17 Dec 2012 00:00:00 +0000
  64. 23 def self.civil_from_format(utc_or_local, year, month = 1, day = 1, hour = 0, min = 0, sec = 0)
  65. if utc_or_local.to_sym == :local
  66. offset = ::Time.local(year, month, day).utc_offset.to_r / 86400
  67. else
  68. offset = 0
  69. end
  70. civil(year, month, day, hour, min, sec, offset)
  71. end
  72. # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
  73. 23 def to_f
  74. seconds_since_unix_epoch.to_f + sec_fraction
  75. end
  76. # Converts +self+ to an integer number of seconds since the Unix epoch.
  77. 23 def to_i
  78. seconds_since_unix_epoch.to_i
  79. end
  80. # Returns the fraction of a second as microseconds
  81. 23 def usec
  82. (sec_fraction * 1_000_000).to_i
  83. end
  84. # Returns the fraction of a second as nanoseconds
  85. 23 def nsec
  86. (sec_fraction * 1_000_000_000).to_i
  87. end
  88. 23 private
  89. 23 def offset_in_seconds
  90. (offset * 86400).to_i
  91. end
  92. 23 def seconds_since_unix_epoch
  93. (jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
  94. end
  95. end

lib/active_support/core_ext/digest.rb

0.0% lines covered

1 relevant lines. 0 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/digest/uuid"

lib/active_support/core_ext/digest/uuid.rb

42.31% lines covered

26 relevant lines. 11 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "securerandom"
  3. 1 module Digest
  4. 1 module UUID
  5. 1 DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
  6. 1 URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
  7. 1 OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
  8. 1 X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
  9. # Generates a v5 non-random UUID (Universally Unique IDentifier).
  10. #
  11. # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
  12. # uuid_from_hash always generates the same UUID for a given name and namespace combination.
  13. #
  14. # See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
  15. 1 def self.uuid_from_hash(hash_class, uuid_namespace, name)
  16. if hash_class == Digest::MD5
  17. version = 3
  18. elsif hash_class == Digest::SHA1
  19. version = 5
  20. else
  21. raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
  22. end
  23. hash = hash_class.new
  24. hash.update(uuid_namespace)
  25. hash.update(name)
  26. ary = hash.digest.unpack("NnnnnN")
  27. ary[2] = (ary[2] & 0x0FFF) | (version << 12)
  28. ary[3] = (ary[3] & 0x3FFF) | 0x8000
  29. "%08x-%04x-%04x-%04x-%04x%08x" % ary
  30. end
  31. # Convenience method for uuid_from_hash using Digest::MD5.
  32. 1 def self.uuid_v3(uuid_namespace, name)
  33. uuid_from_hash(Digest::MD5, uuid_namespace, name)
  34. end
  35. # Convenience method for uuid_from_hash using Digest::SHA1.
  36. 1 def self.uuid_v5(uuid_namespace, name)
  37. uuid_from_hash(Digest::SHA1, uuid_namespace, name)
  38. end
  39. # Convenience method for SecureRandom.uuid.
  40. 1 def self.uuid_v4
  41. SecureRandom.uuid
  42. end
  43. end
  44. end

lib/active_support/core_ext/enumerable.rb

33.75% lines covered

80 relevant lines. 27 lines covered and 53 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module Enumerable
  3. 23 INDEX_WITH_DEFAULT = Object.new
  4. 23 private_constant :INDEX_WITH_DEFAULT
  5. # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
  6. # when we omit an identity.
  7. # :stopdoc:
  8. # We can't use Refinements here because Refinements with Module which will be prepended
  9. # doesn't work well https://bugs.ruby-lang.org/issues/13446
  10. 23 alias :_original_sum_with_required_identity :sum
  11. 23 private :_original_sum_with_required_identity
  12. # :startdoc:
  13. # Calculates a sum from the elements.
  14. #
  15. # payments.sum { |p| p.price * p.tax_rate }
  16. # payments.sum(&:price)
  17. #
  18. # The latter is a shortcut for:
  19. #
  20. # payments.inject(0) { |sum, p| sum + p.price }
  21. #
  22. # It can also calculate the sum without the use of a block.
  23. #
  24. # [5, 15, 10].sum # => 30
  25. # ['foo', 'bar'].sum # => "foobar"
  26. # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
  27. #
  28. # The default sum of an empty list is zero. You can override this default:
  29. #
  30. # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
  31. 23 def sum(identity = nil, &block)
  32. if identity
  33. _original_sum_with_required_identity(identity, &block)
  34. elsif block_given?
  35. map(&block).sum(identity)
  36. else
  37. inject(:+) || 0
  38. end
  39. end
  40. # Convert an enumerable to a hash, using the block result as the key and the
  41. # element as the value.
  42. #
  43. # people.index_by(&:login)
  44. # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
  45. #
  46. # people.index_by { |person| "#{person.first_name} #{person.last_name}" }
  47. # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
  48. 23 def index_by
  49. if block_given?
  50. result = {}
  51. each { |elem| result[yield(elem)] = elem }
  52. result
  53. else
  54. to_enum(:index_by) { size if respond_to?(:size) }
  55. end
  56. end
  57. # Convert an enumerable to a hash, using the element as the key and the block
  58. # result as the value.
  59. #
  60. # post = Post.new(title: "hey there", body: "what's up?")
  61. #
  62. # %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
  63. # # => { title: "hey there", body: "what's up?" }
  64. #
  65. # If an argument is passed instead of a block, it will be used as the value
  66. # for all elements:
  67. #
  68. # %i( created_at updated_at ).index_with(Time.now)
  69. # # => { created_at: 2020-03-09 22:31:47, updated_at: 2020-03-09 22:31:47 }
  70. 23 def index_with(default = INDEX_WITH_DEFAULT)
  71. if block_given?
  72. result = {}
  73. each { |elem| result[elem] = yield(elem) }
  74. result
  75. elsif default != INDEX_WITH_DEFAULT
  76. result = {}
  77. each { |elem| result[elem] = default }
  78. result
  79. else
  80. to_enum(:index_with) { size if respond_to?(:size) }
  81. end
  82. end
  83. # Returns +true+ if the enumerable has more than 1 element. Functionally
  84. # equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
  85. # much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
  86. # if more than one person is over 26.
  87. 23 def many?
  88. cnt = 0
  89. if block_given?
  90. any? do |element|
  91. cnt += 1 if yield element
  92. cnt > 1
  93. end
  94. else
  95. any? { (cnt += 1) > 1 }
  96. end
  97. end
  98. # Returns a new array that includes the passed elements.
  99. #
  100. # [ 1, 2, 3 ].including(4, 5)
  101. # # => [ 1, 2, 3, 4, 5 ]
  102. #
  103. # ["David", "Rafael"].including %w[ Aaron Todd ]
  104. # # => ["David", "Rafael", "Aaron", "Todd"]
  105. 23 def including(*elements)
  106. to_a.including(*elements)
  107. end
  108. # The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
  109. # collection does not include the object.
  110. 23 def exclude?(object)
  111. !include?(object)
  112. end
  113. # Returns a copy of the enumerable excluding the specified elements.
  114. #
  115. # ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd"
  116. # # => ["David", "Rafael"]
  117. #
  118. # ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ]
  119. # # => ["David", "Rafael"]
  120. #
  121. # {foo: 1, bar: 2, baz: 3}.excluding :bar
  122. # # => {foo: 1, baz: 3}
  123. 23 def excluding(*elements)
  124. elements.flatten!(1)
  125. reject { |element| elements.include?(element) }
  126. end
  127. # Alias for #excluding.
  128. 23 def without(*elements)
  129. excluding(*elements)
  130. end
  131. # Extract the given key from each element in the enumerable.
  132. #
  133. # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
  134. # # => ["David", "Rafael", "Aaron"]
  135. #
  136. # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
  137. # # => [[1, "David"], [2, "Rafael"]]
  138. 23 def pluck(*keys)
  139. if keys.many?
  140. map { |element| keys.map { |key| element[key] } }
  141. else
  142. key = keys.first
  143. map { |element| element[key] }
  144. end
  145. end
  146. # Extract the given key from the first element in the enumerable.
  147. #
  148. # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pick(:name)
  149. # # => "David"
  150. #
  151. # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pick(:id, :name)
  152. # # => [1, "David"]
  153. 23 def pick(*keys)
  154. return if none?
  155. if keys.many?
  156. keys.map { |key| first[key] }
  157. else
  158. first[keys.first]
  159. end
  160. end
  161. # Returns a new +Array+ without the blank items.
  162. # Uses Object#blank? for determining if an item is blank.
  163. #
  164. # [1, "", nil, 2, " ", [], {}, false, true].compact_blank
  165. # # => [1, 2, true]
  166. #
  167. # Set.new([nil, "", 1, 2])
  168. # # => [2, 1] (or [1, 2])
  169. #
  170. # When called on a +Hash+, returns a new +Hash+ without the blank values.
  171. #
  172. # { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
  173. # #=> { b: 1, f: true }
  174. 23 def compact_blank
  175. reject(&:blank?)
  176. end
  177. end
  178. 23 class Hash
  179. # Hash#reject has its own definition, so this needs one too.
  180. 23 def compact_blank #:nodoc:
  181. reject { |_k, v| v.blank? }
  182. end
  183. # Removes all blank values from the +Hash+ in place and returns self.
  184. # Uses Object#blank? for determining if a value is blank.
  185. #
  186. # h = { a: "", b: 1, c: nil, d: [], e: false, f: true }
  187. # h.compact_blank!
  188. # # => { b: 1, f: true }
  189. 23 def compact_blank!
  190. # use delete_if rather than reject! because it always returns self even if nothing changed
  191. delete_if { |_k, v| v.blank? }
  192. end
  193. end
  194. 23 class Range #:nodoc:
  195. # Optimize range sum to use arithmetic progression if a block is not given and
  196. # we have a range of numeric values.
  197. 23 def sum(identity = nil)
  198. if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
  199. super
  200. else
  201. actual_last = exclude_end? ? (last - 1) : last
  202. if actual_last >= first
  203. sum = identity || 0
  204. sum + (actual_last - first + 1) * (actual_last + first) / 2
  205. else
  206. identity || 0
  207. end
  208. end
  209. end
  210. end
  211. # Using Refinements here in order not to expose our internal method
  212. 23 using Module.new {
  213. 23 refine Array do
  214. 23 alias :orig_sum :sum
  215. end
  216. }
  217. 23 class Array #:nodoc:
  218. # Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
  219. 23 def sum(init = nil, &block)
  220. if init.is_a?(Numeric) || first.is_a?(Numeric)
  221. init ||= 0
  222. orig_sum(init, &block)
  223. else
  224. super
  225. end
  226. end
  227. # Removes all blank elements from the +Array+ in place and returns self.
  228. # Uses Object#blank? for determining if an item is blank.
  229. #
  230. # a = [1, "", nil, 2, " ", [], {}, false, true]
  231. # a.compact_blank!
  232. # # => [1, 2, true]
  233. 23 def compact_blank!
  234. # use delete_if rather than reject! because it always returns self even if nothing changed
  235. delete_if(&:blank?)
  236. end
  237. end

lib/active_support/core_ext/file.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/file/atomic"

lib/active_support/core_ext/file/atomic.rb

17.39% lines covered

23 relevant lines. 4 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "fileutils"
  3. 1 class File
  4. # Write to a file atomically. Useful for situations where you don't
  5. # want other processes or threads to see half-written files.
  6. #
  7. # File.atomic_write('important.file') do |file|
  8. # file.write('hello')
  9. # end
  10. #
  11. # This method needs to create a temporary file. By default it will create it
  12. # in the same directory as the destination file. If you don't like this
  13. # behavior you can provide a different directory but it must be on the
  14. # same physical filesystem as the file you're trying to write.
  15. #
  16. # File.atomic_write('/data/something.important', '/data/tmp') do |file|
  17. # file.write('hello')
  18. # end
  19. 1 def self.atomic_write(file_name, temp_dir = dirname(file_name))
  20. require "tempfile" unless defined?(Tempfile)
  21. Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
  22. temp_file.binmode
  23. return_val = yield temp_file
  24. temp_file.close
  25. old_stat = if exist?(file_name)
  26. # Get original file permissions
  27. stat(file_name)
  28. else
  29. # If not possible, probe which are the default permissions in the
  30. # destination directory.
  31. probe_stat_in(dirname(file_name))
  32. end
  33. if old_stat
  34. # Set correct permissions on new file
  35. begin
  36. chown(old_stat.uid, old_stat.gid, temp_file.path)
  37. # This operation will affect filesystem ACL's
  38. chmod(old_stat.mode, temp_file.path)
  39. rescue Errno::EPERM, Errno::EACCES
  40. # Changing file ownership failed, moving on.
  41. end
  42. end
  43. # Overwrite original file with temp file
  44. rename(temp_file.path, file_name)
  45. return_val
  46. end
  47. end
  48. # Private utility method.
  49. 1 def self.probe_stat_in(dir) #:nodoc:
  50. basename = [
  51. ".permissions_check",
  52. Thread.current.object_id,
  53. Process.pid,
  54. rand(1000000)
  55. ].join(".")
  56. file_name = join(dir, basename)
  57. FileUtils.touch(file_name)
  58. stat(file_name)
  59. ensure
  60. FileUtils.rm_f(file_name) if file_name
  61. end
  62. end

lib/active_support/core_ext/hash.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/conversions"
  3. 1 require "active_support/core_ext/hash/deep_merge"
  4. 1 require "active_support/core_ext/hash/deep_transform_values"
  5. 1 require "active_support/core_ext/hash/except"
  6. 1 require "active_support/core_ext/hash/indifferent_access"
  7. 1 require "active_support/core_ext/hash/keys"
  8. 1 require "active_support/core_ext/hash/reverse_merge"
  9. 1 require "active_support/core_ext/hash/slice"

lib/active_support/core_ext/hash/compact.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation"
  3. ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#compact and Hash#compact! natively, so requiring active_support/core_ext/hash/compact is no longer necessary. Requiring it will raise LoadError in Rails 6.1."

lib/active_support/core_ext/hash/conversions.rb

36.78% lines covered

87 relevant lines. 32 lines covered and 55 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/xml_mini"
  3. 1 require "active_support/core_ext/object/blank"
  4. 1 require "active_support/core_ext/object/to_param"
  5. 1 require "active_support/core_ext/object/to_query"
  6. 1 require "active_support/core_ext/object/try"
  7. 1 require "active_support/core_ext/array/wrap"
  8. 1 require "active_support/core_ext/hash/reverse_merge"
  9. 1 require "active_support/core_ext/string/inflections"
  10. 1 class Hash
  11. # Returns a string containing an XML representation of its receiver:
  12. #
  13. # { foo: 1, bar: 2 }.to_xml
  14. # # =>
  15. # # <?xml version="1.0" encoding="UTF-8"?>
  16. # # <hash>
  17. # # <foo type="integer">1</foo>
  18. # # <bar type="integer">2</bar>
  19. # # </hash>
  20. #
  21. # To do so, the method loops over the pairs and builds nodes that depend on
  22. # the _values_. Given a pair +key+, +value+:
  23. #
  24. # * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
  25. #
  26. # * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
  27. # and +key+ singularized as <tt>:children</tt>.
  28. #
  29. # * If +value+ is a callable object it must expect one or two arguments. Depending
  30. # on the arity, the callable is invoked with the +options+ hash as first argument
  31. # with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
  32. # callable can add nodes by using <tt>options[:builder]</tt>.
  33. #
  34. # {foo: lambda { |options, key| options[:builder].b(key) }}.to_xml
  35. # # => "<b>foo</b>"
  36. #
  37. # * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
  38. #
  39. # class Foo
  40. # def to_xml(options)
  41. # options[:builder].bar 'fooing!'
  42. # end
  43. # end
  44. #
  45. # { foo: Foo.new }.to_xml(skip_instruct: true)
  46. # # =>
  47. # # <hash>
  48. # # <bar>fooing!</bar>
  49. # # </hash>
  50. #
  51. # * Otherwise, a node with +key+ as tag is created with a string representation of
  52. # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
  53. # Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
  54. # added as well according to the following mapping:
  55. #
  56. # XML_TYPE_NAMES = {
  57. # "Symbol" => "symbol",
  58. # "Integer" => "integer",
  59. # "BigDecimal" => "decimal",
  60. # "Float" => "float",
  61. # "TrueClass" => "boolean",
  62. # "FalseClass" => "boolean",
  63. # "Date" => "date",
  64. # "DateTime" => "dateTime",
  65. # "Time" => "dateTime"
  66. # }
  67. #
  68. # By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
  69. #
  70. # The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
  71. # configure your own builder with the <tt>:builder</tt> option. The method also accepts
  72. # options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
  73. 1 def to_xml(options = {})
  74. require "active_support/builder" unless defined?(Builder::XmlMarkup)
  75. options = options.dup
  76. options[:indent] ||= 2
  77. options[:root] ||= "hash"
  78. options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
  79. builder = options[:builder]
  80. builder.instruct! unless options.delete(:skip_instruct)
  81. root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
  82. builder.tag!(root) do
  83. each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
  84. yield builder if block_given?
  85. end
  86. end
  87. 1 class << self
  88. # Returns a Hash containing a collection of pairs when the key is the node name and the value is
  89. # its content
  90. #
  91. # xml = <<-XML
  92. # <?xml version="1.0" encoding="UTF-8"?>
  93. # <hash>
  94. # <foo type="integer">1</foo>
  95. # <bar type="integer">2</bar>
  96. # </hash>
  97. # XML
  98. #
  99. # hash = Hash.from_xml(xml)
  100. # # => {"hash"=>{"foo"=>1, "bar"=>2}}
  101. #
  102. # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
  103. # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to
  104. # parse this XML.
  105. #
  106. # Custom +disallowed_types+ can also be passed in the form of an
  107. # array.
  108. #
  109. # xml = <<-XML
  110. # <?xml version="1.0" encoding="UTF-8"?>
  111. # <hash>
  112. # <foo type="integer">1</foo>
  113. # <bar type="string">"David"</bar>
  114. # </hash>
  115. # XML
  116. #
  117. # hash = Hash.from_xml(xml, ['integer'])
  118. # # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer"
  119. #
  120. # Note that passing custom disallowed types will override the default types,
  121. # which are Symbol and YAML.
  122. 1 def from_xml(xml, disallowed_types = nil)
  123. ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
  124. end
  125. # Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.
  126. 1 def from_trusted_xml(xml)
  127. from_xml xml, []
  128. end
  129. end
  130. end
  131. 1 module ActiveSupport
  132. 1 class XMLConverter # :nodoc:
  133. # Raised if the XML contains attributes with type="yaml" or
  134. # type="symbol". Read Hash#from_xml for more details.
  135. 1 class DisallowedType < StandardError
  136. 1 def initialize(type)
  137. super "Disallowed type attribute: #{type.inspect}"
  138. end
  139. end
  140. 1 DISALLOWED_TYPES = %w(symbol yaml)
  141. 1 def initialize(xml, disallowed_types = nil)
  142. @xml = normalize_keys(XmlMini.parse(xml))
  143. @disallowed_types = disallowed_types || DISALLOWED_TYPES
  144. end
  145. 1 def to_h
  146. deep_to_h(@xml)
  147. end
  148. 1 private
  149. 1 def normalize_keys(params)
  150. case params
  151. when Hash
  152. Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ]
  153. when Array
  154. params.map { |v| normalize_keys(v) }
  155. else
  156. params
  157. end
  158. end
  159. 1 def deep_to_h(value)
  160. case value
  161. when Hash
  162. process_hash(value)
  163. when Array
  164. process_array(value)
  165. when String
  166. value
  167. else
  168. raise "can't typecast #{value.class.name} - #{value.inspect}"
  169. end
  170. end
  171. 1 def process_hash(value)
  172. if value.include?("type") && !value["type"].is_a?(Hash) && @disallowed_types.include?(value["type"])
  173. raise DisallowedType, value["type"]
  174. end
  175. if become_array?(value)
  176. _, entries = Array.wrap(value.detect { |k, v| not v.is_a?(String) })
  177. if entries.nil? || value["__content__"].try(:empty?)
  178. []
  179. else
  180. case entries
  181. when Array
  182. entries.collect { |v| deep_to_h(v) }
  183. when Hash
  184. [deep_to_h(entries)]
  185. else
  186. raise "can't typecast #{entries.inspect}"
  187. end
  188. end
  189. elsif become_content?(value)
  190. process_content(value)
  191. elsif become_empty_string?(value)
  192. ""
  193. elsif become_hash?(value)
  194. xml_value = value.transform_values { |v| deep_to_h(v) }
  195. # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
  196. # how multipart uploaded files from HTML appear
  197. xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
  198. end
  199. end
  200. 1 def become_content?(value)
  201. value["type"] == "file" || (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
  202. end
  203. 1 def become_array?(value)
  204. value["type"] == "array"
  205. end
  206. 1 def become_empty_string?(value)
  207. # { "string" => true }
  208. # No tests fail when the second term is removed.
  209. value["type"] == "string" && value["nil"] != "true"
  210. end
  211. 1 def become_hash?(value)
  212. !nothing?(value) && !garbage?(value)
  213. end
  214. 1 def nothing?(value)
  215. # blank or nil parsed values are represented by nil
  216. value.blank? || value["nil"] == "true"
  217. end
  218. 1 def garbage?(value)
  219. # If the type is the only element which makes it then
  220. # this still makes the value nil, except if type is
  221. # an XML node(where type['value'] is a Hash)
  222. value["type"] && !value["type"].is_a?(::Hash) && value.size == 1
  223. end
  224. 1 def process_content(value)
  225. content = value["__content__"]
  226. if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
  227. parser.arity == 1 ? parser.call(content) : parser.call(content, value)
  228. else
  229. content
  230. end
  231. end
  232. 1 def process_array(value)
  233. value.map! { |i| deep_to_h(i) }
  234. value.length > 1 ? value : value.first
  235. end
  236. end
  237. end

lib/active_support/core_ext/hash/deep_merge.rb

30.0% lines covered

10 relevant lines. 3 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 class Hash
  3. # Returns a new hash with +self+ and +other_hash+ merged recursively.
  4. #
  5. # h1 = { a: true, b: { c: [1, 2, 3] } }
  6. # h2 = { a: false, b: { x: [3, 4, 5] } }
  7. #
  8. # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
  9. #
  10. # Like with Hash#merge in the standard library, a block can be provided
  11. # to merge values:
  12. #
  13. # h1 = { a: 100, b: 200, c: { c1: 100 } }
  14. # h2 = { b: 250, c: { c1: 200 } }
  15. # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
  16. # # => { a: 100, b: 450, c: { c1: 300 } }
  17. 24 def deep_merge(other_hash, &block)
  18. dup.deep_merge!(other_hash, &block)
  19. end
  20. # Same as +deep_merge+, but modifies +self+.
  21. 24 def deep_merge!(other_hash, &block)
  22. merge!(other_hash) do |key, this_val, other_val|
  23. if this_val.is_a?(Hash) && other_val.is_a?(Hash)
  24. this_val.deep_merge(other_val, &block)
  25. elsif block_given?
  26. block.call(key, this_val, other_val)
  27. else
  28. other_val
  29. end
  30. end
  31. end
  32. end

lib/active_support/core_ext/hash/deep_transform_values.rb

37.5% lines covered

16 relevant lines. 6 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Hash
  3. # Returns a new hash with all values converted by the block operation.
  4. # This includes the values from the root hash and from all
  5. # nested hashes and arrays.
  6. #
  7. # hash = { person: { name: 'Rob', age: '28' } }
  8. #
  9. # hash.deep_transform_values{ |value| value.to_s.upcase }
  10. # # => {person: {name: "ROB", age: "28"}}
  11. 1 def deep_transform_values(&block)
  12. _deep_transform_values_in_object(self, &block)
  13. end
  14. # Destructively converts all values by using the block operation.
  15. # This includes the values from the root hash and from all
  16. # nested hashes and arrays.
  17. 1 def deep_transform_values!(&block)
  18. _deep_transform_values_in_object!(self, &block)
  19. end
  20. 1 private
  21. # Support methods for deep transforming nested hashes and arrays.
  22. 1 def _deep_transform_values_in_object(object, &block)
  23. case object
  24. when Hash
  25. object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
  26. when Array
  27. object.map { |e| _deep_transform_values_in_object(e, &block) }
  28. else
  29. yield(object)
  30. end
  31. end
  32. 1 def _deep_transform_values_in_object!(object, &block)
  33. case object
  34. when Hash
  35. object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
  36. when Array
  37. object.map! { |e| _deep_transform_values_in_object!(e, &block) }
  38. else
  39. yield(object)
  40. end
  41. end
  42. end

lib/active_support/core_ext/hash/except.rb

50.0% lines covered

6 relevant lines. 3 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 class Hash
  3. # Returns a hash that includes everything except given keys.
  4. # hash = { a: true, b: false, c: nil }
  5. # hash.except(:c) # => { a: true, b: false }
  6. # hash.except(:a, :b) # => { c: nil }
  7. # hash # => { a: true, b: false, c: nil }
  8. #
  9. # This is useful for limiting a set of parameters to everything but a few known toggles:
  10. # @person.update(params[:person].except(:admin))
  11. def except(*keys)
  12. slice(*self.keys - keys)
  13. 24 end unless method_defined?(:except)
  14. # Removes the given keys from hash and returns it.
  15. # hash = { a: true, b: false, c: nil }
  16. # hash.except!(:c) # => { a: true, b: false }
  17. # hash # => { a: true, b: false }
  18. 24 def except!(*keys)
  19. keys.each { |key| delete(key) }
  20. self
  21. end
  22. end

lib/active_support/core_ext/hash/indifferent_access.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/hash_with_indifferent_access"
  3. 1 class Hash
  4. # Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
  5. #
  6. # { a: 1 }.with_indifferent_access['a'] # => 1
  7. 1 def with_indifferent_access
  8. ActiveSupport::HashWithIndifferentAccess.new(self)
  9. end
  10. # Called when object is nested under an object that receives
  11. # #with_indifferent_access. This method will be called on the current object
  12. # by the enclosing object and is aliased to #with_indifferent_access by
  13. # default. Subclasses of Hash may overwrite this method to return +self+ if
  14. # converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
  15. # desirable.
  16. #
  17. # b = { b: 1 }
  18. # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
  19. # # => {"b"=>1}
  20. 1 alias nested_under_indifferent_access with_indifferent_access
  21. end

lib/active_support/core_ext/hash/keys.rb

39.53% lines covered

43 relevant lines. 17 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 class Hash
  3. # Returns a new hash with all keys converted to strings.
  4. #
  5. # hash = { name: 'Rob', age: '28' }
  6. #
  7. # hash.stringify_keys
  8. # # => {"name"=>"Rob", "age"=>"28"}
  9. 23 def stringify_keys
  10. transform_keys(&:to_s)
  11. end
  12. # Destructively converts all keys to strings. Same as
  13. # +stringify_keys+, but modifies +self+.
  14. 23 def stringify_keys!
  15. transform_keys!(&:to_s)
  16. end
  17. # Returns a new hash with all keys converted to symbols, as long as
  18. # they respond to +to_sym+.
  19. #
  20. # hash = { 'name' => 'Rob', 'age' => '28' }
  21. #
  22. # hash.symbolize_keys
  23. # # => {:name=>"Rob", :age=>"28"}
  24. 23 def symbolize_keys
  25. transform_keys { |key| key.to_sym rescue key }
  26. end
  27. 23 alias_method :to_options, :symbolize_keys
  28. # Destructively converts all keys to symbols, as long as they respond
  29. # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
  30. 23 def symbolize_keys!
  31. transform_keys! { |key| key.to_sym rescue key }
  32. end
  33. 23 alias_method :to_options!, :symbolize_keys!
  34. # Validates all keys in a hash match <tt>*valid_keys</tt>, raising
  35. # +ArgumentError+ on a mismatch.
  36. #
  37. # Note that keys are treated differently than HashWithIndifferentAccess,
  38. # meaning that string and symbol keys will not match.
  39. #
  40. # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
  41. # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
  42. # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
  43. 23 def assert_valid_keys(*valid_keys)
  44. valid_keys.flatten!
  45. each_key do |k|
  46. unless valid_keys.include?(k)
  47. raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
  48. end
  49. end
  50. end
  51. # Returns a new hash with all keys converted by the block operation.
  52. # This includes the keys from the root hash and from all
  53. # nested hashes and arrays.
  54. #
  55. # hash = { person: { name: 'Rob', age: '28' } }
  56. #
  57. # hash.deep_transform_keys{ |key| key.to_s.upcase }
  58. # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
  59. 23 def deep_transform_keys(&block)
  60. _deep_transform_keys_in_object(self, &block)
  61. end
  62. # Destructively converts all keys by using the block operation.
  63. # This includes the keys from the root hash and from all
  64. # nested hashes and arrays.
  65. 23 def deep_transform_keys!(&block)
  66. _deep_transform_keys_in_object!(self, &block)
  67. end
  68. # Returns a new hash with all keys converted to strings.
  69. # This includes the keys from the root hash and from all
  70. # nested hashes and arrays.
  71. #
  72. # hash = { person: { name: 'Rob', age: '28' } }
  73. #
  74. # hash.deep_stringify_keys
  75. # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
  76. 23 def deep_stringify_keys
  77. deep_transform_keys(&:to_s)
  78. end
  79. # Destructively converts all keys to strings.
  80. # This includes the keys from the root hash and from all
  81. # nested hashes and arrays.
  82. 23 def deep_stringify_keys!
  83. deep_transform_keys!(&:to_s)
  84. end
  85. # Returns a new hash with all keys converted to symbols, as long as
  86. # they respond to +to_sym+. This includes the keys from the root hash
  87. # and from all nested hashes and arrays.
  88. #
  89. # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
  90. #
  91. # hash.deep_symbolize_keys
  92. # # => {:person=>{:name=>"Rob", :age=>"28"}}
  93. 23 def deep_symbolize_keys
  94. deep_transform_keys { |key| key.to_sym rescue key }
  95. end
  96. # Destructively converts all keys to symbols, as long as they respond
  97. # to +to_sym+. This includes the keys from the root hash and from all
  98. # nested hashes and arrays.
  99. 23 def deep_symbolize_keys!
  100. deep_transform_keys! { |key| key.to_sym rescue key }
  101. end
  102. 23 private
  103. # Support methods for deep transforming nested hashes and arrays.
  104. 23 def _deep_transform_keys_in_object(object, &block)
  105. case object
  106. when Hash
  107. object.each_with_object({}) do |(key, value), result|
  108. result[yield(key)] = _deep_transform_keys_in_object(value, &block)
  109. end
  110. when Array
  111. object.map { |e| _deep_transform_keys_in_object(e, &block) }
  112. else
  113. object
  114. end
  115. end
  116. 23 def _deep_transform_keys_in_object!(object, &block)
  117. case object
  118. when Hash
  119. object.keys.each do |key|
  120. value = object.delete(key)
  121. object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
  122. end
  123. object
  124. when Array
  125. object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
  126. else
  127. object
  128. end
  129. end
  130. end

lib/active_support/core_ext/hash/reverse_merge.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Hash
  3. # Merges the caller into +other_hash+. For example,
  4. #
  5. # options = options.reverse_merge(size: 25, velocity: 10)
  6. #
  7. # is equivalent to
  8. #
  9. # options = { size: 25, velocity: 10 }.merge(options)
  10. #
  11. # This is particularly useful for initializing an options hash
  12. # with default values.
  13. 1 def reverse_merge(other_hash)
  14. other_hash.merge(self)
  15. end
  16. 1 alias_method :with_defaults, :reverse_merge
  17. # Destructive +reverse_merge+.
  18. 1 def reverse_merge!(other_hash)
  19. replace(reverse_merge(other_hash))
  20. end
  21. 1 alias_method :reverse_update, :reverse_merge!
  22. 1 alias_method :with_defaults!, :reverse_merge!
  23. end

lib/active_support/core_ext/hash/slice.rb

30.0% lines covered

10 relevant lines. 3 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 class Hash
  3. # Replaces the hash with only the given keys.
  4. # Returns a hash containing the removed key/value pairs.
  5. #
  6. # hash = { a: 1, b: 2, c: 3, d: 4 }
  7. # hash.slice!(:a, :b) # => {:c=>3, :d=>4}
  8. # hash # => {:a=>1, :b=>2}
  9. 24 def slice!(*keys)
  10. omit = slice(*self.keys - keys)
  11. hash = slice(*keys)
  12. hash.default = default
  13. hash.default_proc = default_proc if default_proc
  14. replace(hash)
  15. omit
  16. end
  17. # Removes and returns the key/value pairs matching the given keys.
  18. #
  19. # { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
  20. # { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
  21. 24 def extract!(*keys)
  22. keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
  23. end
  24. end

lib/active_support/core_ext/hash/transform_values.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation"
  3. ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1."

lib/active_support/core_ext/integer.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/integer/multiple"
  3. 1 require "active_support/core_ext/integer/inflections"
  4. 1 require "active_support/core_ext/integer/time"

lib/active_support/core_ext/integer/inflections.rb

66.67% lines covered

6 relevant lines. 4 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/inflector"
  3. 1 class Integer
  4. # Ordinalize turns a number into an ordinal string used to denote the
  5. # position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
  6. #
  7. # 1.ordinalize # => "1st"
  8. # 2.ordinalize # => "2nd"
  9. # 1002.ordinalize # => "1002nd"
  10. # 1003.ordinalize # => "1003rd"
  11. # -11.ordinalize # => "-11th"
  12. # -1001.ordinalize # => "-1001st"
  13. 1 def ordinalize
  14. ActiveSupport::Inflector.ordinalize(self)
  15. end
  16. # Ordinal returns the suffix used to denote the position
  17. # in an ordered sequence such as 1st, 2nd, 3rd, 4th.
  18. #
  19. # 1.ordinal # => "st"
  20. # 2.ordinal # => "nd"
  21. # 1002.ordinal # => "nd"
  22. # 1003.ordinal # => "rd"
  23. # -11.ordinal # => "th"
  24. # -1001.ordinal # => "st"
  25. 1 def ordinal
  26. ActiveSupport::Inflector.ordinal(self)
  27. end
  28. end

lib/active_support/core_ext/integer/multiple.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Integer
  3. # Check whether the integer is evenly divisible by the argument.
  4. #
  5. # 0.multiple_of?(0) # => true
  6. # 6.multiple_of?(5) # => false
  7. # 10.multiple_of?(2) # => true
  8. 1 def multiple_of?(number)
  9. number == 0 ? self == 0 : self % number == 0
  10. end
  11. end

lib/active_support/core_ext/integer/time.rb

77.78% lines covered

9 relevant lines. 7 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/duration"
  3. 2 require "active_support/core_ext/numeric/time"
  4. 2 class Integer
  5. # Returns a Duration instance matching the number of months provided.
  6. #
  7. # 2.months # => 2 months
  8. 2 def months
  9. ActiveSupport::Duration.months(self)
  10. end
  11. 2 alias :month :months
  12. # Returns a Duration instance matching the number of years provided.
  13. #
  14. # 2.years # => 2 years
  15. 2 def years
  16. ActiveSupport::Duration.years(self)
  17. end
  18. 2 alias :year :years
  19. end

lib/active_support/core_ext/kernel.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/kernel/concern"
  3. 1 require "active_support/core_ext/kernel/reporting"
  4. 1 require "active_support/core_ext/kernel/singleton_class"

lib/active_support/core_ext/kernel/concern.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/concerning"
  3. 1 module Kernel
  4. 1 module_function
  5. # A shortcut to define a toplevel concern, not within a module.
  6. #
  7. # See Module::Concerning for more.
  8. 1 def concern(topic, &module_definition)
  9. Object.concern topic, &module_definition
  10. end
  11. end

lib/active_support/core_ext/kernel/reporting.rb

83.33% lines covered

12 relevant lines. 10 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module Kernel
  3. 24 module_function
  4. # Sets $VERBOSE to +nil+ for the duration of the block and back to its original
  5. # value afterwards.
  6. #
  7. # silence_warnings do
  8. # value = noisy_call # no warning voiced
  9. # end
  10. #
  11. # noisy_call # warning voiced
  12. 24 def silence_warnings
  13. 48 with_warnings(nil) { yield }
  14. end
  15. # Sets $VERBOSE to +true+ for the duration of the block and back to its
  16. # original value afterwards.
  17. 24 def enable_warnings
  18. with_warnings(true) { yield }
  19. end
  20. # Sets $VERBOSE for the duration of the block and back to its original
  21. # value afterwards.
  22. 24 def with_warnings(flag)
  23. 24 old_verbose, $VERBOSE = $VERBOSE, flag
  24. 24 yield
  25. ensure
  26. 24 $VERBOSE = old_verbose
  27. end
  28. # Blocks and ignores any exception passed as argument if raised within the block.
  29. #
  30. # suppress(ZeroDivisionError) do
  31. # 1/0
  32. # puts 'This code is NOT reached'
  33. # end
  34. #
  35. # puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
  36. 24 def suppress(*exception_classes)
  37. yield
  38. rescue *exception_classes
  39. end
  40. end

lib/active_support/core_ext/kernel/singleton_class.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Kernel
  3. # class_eval on an object acts like singleton_class.class_eval.
  4. 1 def class_eval(*args, &block)
  5. singleton_class.class_eval(*args, &block)
  6. end
  7. end

lib/active_support/core_ext/load_error.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class LoadError
  3. # Returns true if the given path name (except perhaps for the ".rb"
  4. # extension) is the missing file which caused the exception to be raised.
  5. 3 def is_missing?(location)
  6. location.delete_suffix(".rb") == path.to_s.delete_suffix(".rb")
  7. end
  8. end

lib/active_support/core_ext/marshal.rb

50.0% lines covered

12 relevant lines. 6 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/core_ext/string/inflections"
  3. 3 module ActiveSupport
  4. 3 module MarshalWithAutoloading # :nodoc:
  5. 3 def load(source, proc = nil)
  6. 55997 super(source, proc)
  7. rescue ArgumentError, NameError => exc
  8. if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
  9. # try loading the class/module
  10. loaded = $1.constantize
  11. raise unless $1 == loaded.name
  12. # if it is an IO we need to go back to read the object
  13. source.rewind if source.respond_to?(:rewind)
  14. retry
  15. else
  16. raise exc
  17. end
  18. end
  19. end
  20. end
  21. 3 Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading)

lib/active_support/core_ext/module.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/aliasing"
  3. 1 require "active_support/core_ext/module/introspection"
  4. 1 require "active_support/core_ext/module/anonymous"
  5. 1 require "active_support/core_ext/module/attribute_accessors"
  6. 1 require "active_support/core_ext/module/attribute_accessors_per_thread"
  7. 1 require "active_support/core_ext/module/attr_internal"
  8. 1 require "active_support/core_ext/module/concerning"
  9. 1 require "active_support/core_ext/module/delegation"
  10. 1 require "active_support/core_ext/module/deprecation"
  11. 1 require "active_support/core_ext/module/redefine_method"
  12. 1 require "active_support/core_ext/module/remove_method"

lib/active_support/core_ext/module/aliasing.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class Module
  3. # Allows you to make aliases for attributes, which includes
  4. # getter, setter, and a predicate.
  5. #
  6. # class Content < ActiveRecord::Base
  7. # # has a title attribute
  8. # end
  9. #
  10. # class Email < Content
  11. # alias_attribute :subject, :title
  12. # end
  13. #
  14. # e = Email.find(1)
  15. # e.title # => "Superstars"
  16. # e.subject # => "Superstars"
  17. # e.subject? # => true
  18. # e.subject = "Megastars"
  19. # e.title # => "Megastars"
  20. 3 def alias_attribute(new_name, old_name)
  21. # The following reader methods use an explicit `self` receiver in order to
  22. # support aliases that start with an uppercase letter. Otherwise, they would
  23. # be resolved as constants instead.
  24. 2 module_eval <<-STR, __FILE__, __LINE__ + 1
  25. def #{new_name}; self.#{old_name}; end # def subject; self.title; end
  26. def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
  27. def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
  28. STR
  29. end
  30. end

lib/active_support/core_ext/module/anonymous.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class Module
  3. # A module may or may not have a name.
  4. #
  5. # module M; end
  6. # M.name # => "M"
  7. #
  8. # m = Module.new
  9. # m.name # => nil
  10. #
  11. # +anonymous?+ method returns true if module does not have a name, false otherwise:
  12. #
  13. # Module.new.anonymous? # => true
  14. #
  15. # module M; end
  16. # M.anonymous? # => false
  17. #
  18. # A module gets a name when it is first assigned to a constant. Either
  19. # via the +module+ or +class+ keyword or by an explicit assignment:
  20. #
  21. # m = Module.new # creates an anonymous module
  22. # m.anonymous? # => true
  23. # M = m # m gets a name here as a side-effect
  24. # m.name # => "M"
  25. # m.anonymous? # => false
  26. 3 def anonymous?
  27. name.nil?
  28. end
  29. end

lib/active_support/core_ext/module/attr_internal.rb

50.0% lines covered

20 relevant lines. 10 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Module
  3. # Declares an attribute reader backed by an internally-named instance variable.
  4. 1 def attr_internal_reader(*attrs)
  5. attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
  6. end
  7. # Declares an attribute writer backed by an internally-named instance variable.
  8. 1 def attr_internal_writer(*attrs)
  9. attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
  10. end
  11. # Declares an attribute reader and writer backed by an internally-named instance
  12. # variable.
  13. 1 def attr_internal_accessor(*attrs)
  14. attr_internal_reader(*attrs)
  15. attr_internal_writer(*attrs)
  16. end
  17. 1 alias_method :attr_internal, :attr_internal_accessor
  18. 2 class << self; attr_accessor :attr_internal_naming_format end
  19. 1 self.attr_internal_naming_format = "@_%s"
  20. 1 private
  21. 1 def attr_internal_ivar_name(attr)
  22. Module.attr_internal_naming_format % attr
  23. end
  24. 1 def attr_internal_define(attr_name, type)
  25. internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
  26. # use native attr_* methods as they are faster on some Ruby implementations
  27. send("attr_#{type}", internal_name)
  28. attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
  29. alias_method attr_name, internal_name
  30. remove_method internal_name
  31. end
  32. end

lib/active_support/core_ext/module/attribute_accessors.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Extends the module object with class/module and instance accessors for
  3. # class/module attributes, just like the native attr* accessors for instance
  4. # attributes.
  5. 24 class Module
  6. # Defines a class attribute and creates a class and instance reader methods.
  7. # The underlying class variable is set to +nil+, if it is not previously
  8. # defined. All class and instance methods created will be public, even if
  9. # this method is called with a private or protected access modifier.
  10. #
  11. # module HairColors
  12. # mattr_reader :hair_colors
  13. # end
  14. #
  15. # HairColors.hair_colors # => nil
  16. # HairColors.class_variable_set("@@hair_colors", [:brown, :black])
  17. # HairColors.hair_colors # => [:brown, :black]
  18. #
  19. # The attribute name must be a valid method name in Ruby.
  20. #
  21. # module Foo
  22. # mattr_reader :"1_Badname"
  23. # end
  24. # # => NameError: invalid attribute name: 1_Badname
  25. #
  26. # To omit the instance reader method, pass
  27. # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
  28. #
  29. # module HairColors
  30. # mattr_reader :hair_colors, instance_reader: false
  31. # end
  32. #
  33. # class Person
  34. # include HairColors
  35. # end
  36. #
  37. # Person.new.hair_colors # => NoMethodError
  38. #
  39. # You can set a default value for the attribute.
  40. #
  41. # module HairColors
  42. # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
  43. # end
  44. #
  45. # class Person
  46. # include HairColors
  47. # end
  48. #
  49. # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
  50. 24 def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
  51. 280 raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
  52. 280 location ||= caller_locations(1, 1).first
  53. 280 definition = []
  54. 280 syms.each do |sym|
  55. 280 raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
  56. 280 definition << "def self.#{sym}; @@#{sym}; end"
  57. 280 if instance_reader && instance_accessor
  58. 252 definition << "def #{sym}; @@#{sym}; end"
  59. end
  60. 280 sym_default_value = (block_given? && default.nil?) ? yield : default
  61. 280 class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
  62. end
  63. 280 module_eval(definition.join(";"), location.path, location.lineno)
  64. end
  65. 24 alias :cattr_reader :mattr_reader
  66. # Defines a class attribute and creates a class and instance writer methods to
  67. # allow assignment to the attribute. All class and instance methods created
  68. # will be public, even if this method is called with a private or protected
  69. # access modifier.
  70. #
  71. # module HairColors
  72. # mattr_writer :hair_colors
  73. # end
  74. #
  75. # class Person
  76. # include HairColors
  77. # end
  78. #
  79. # HairColors.hair_colors = [:brown, :black]
  80. # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
  81. # Person.new.hair_colors = [:blonde, :red]
  82. # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
  83. #
  84. # To omit the instance writer method, pass
  85. # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
  86. #
  87. # module HairColors
  88. # mattr_writer :hair_colors, instance_writer: false
  89. # end
  90. #
  91. # class Person
  92. # include HairColors
  93. # end
  94. #
  95. # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
  96. #
  97. # You can set a default value for the attribute.
  98. #
  99. # module HairColors
  100. # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
  101. # end
  102. #
  103. # class Person
  104. # include HairColors
  105. # end
  106. #
  107. # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
  108. 24 def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil, location: nil)
  109. 186 raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
  110. 186 location ||= caller_locations(1, 1).first
  111. 186 definition = []
  112. 186 syms.each do |sym|
  113. 186 raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
  114. 186 definition << "def self.#{sym}=(val); @@#{sym} = val; end"
  115. 186 if instance_writer && instance_accessor
  116. 110 definition << "def #{sym}=(val); @@#{sym} = val; end"
  117. end
  118. 186 sym_default_value = (block_given? && default.nil?) ? yield : default
  119. 186 class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
  120. end
  121. 186 module_eval(definition.join(";"), location.path, location.lineno)
  122. end
  123. 24 alias :cattr_writer :mattr_writer
  124. # Defines both class and instance accessors for class attributes.
  125. # All class and instance methods created will be public, even if
  126. # this method is called with a private or protected access modifier.
  127. #
  128. # module HairColors
  129. # mattr_accessor :hair_colors
  130. # end
  131. #
  132. # class Person
  133. # include HairColors
  134. # end
  135. #
  136. # HairColors.hair_colors = [:brown, :black, :blonde, :red]
  137. # HairColors.hair_colors # => [:brown, :black, :blonde, :red]
  138. # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
  139. #
  140. # If a subclass changes the value then that would also change the value for
  141. # parent class. Similarly if parent class changes the value then that would
  142. # change the value of subclasses too.
  143. #
  144. # class Citizen < Person
  145. # end
  146. #
  147. # Citizen.new.hair_colors << :blue
  148. # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
  149. #
  150. # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
  151. # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
  152. #
  153. # module HairColors
  154. # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
  155. # end
  156. #
  157. # class Person
  158. # include HairColors
  159. # end
  160. #
  161. # Person.new.hair_colors = [:brown] # => NoMethodError
  162. # Person.new.hair_colors # => NoMethodError
  163. #
  164. # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
  165. #
  166. # module HairColors
  167. # mattr_accessor :hair_colors, instance_accessor: false
  168. # end
  169. #
  170. # class Person
  171. # include HairColors
  172. # end
  173. #
  174. # Person.new.hair_colors = [:brown] # => NoMethodError
  175. # Person.new.hair_colors # => NoMethodError
  176. #
  177. # You can set a default value for the attribute.
  178. #
  179. # module HairColors
  180. # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
  181. # end
  182. #
  183. # class Person
  184. # include HairColors
  185. # end
  186. #
  187. # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
  188. 24 def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
  189. 186 location = caller_locations(1, 1).first
  190. 186 mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, location: location, &blk)
  191. 186 mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default, location: location)
  192. end
  193. 24 alias :cattr_accessor :mattr_accessor
  194. end

lib/active_support/core_ext/module/attribute_accessors_per_thread.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Extends the module object with class/module and instance accessors for
  3. # class/module attributes, just like the native attr* accessors for instance
  4. # attributes, but does so on a per-thread basis.
  5. #
  6. # So the values are scoped within the Thread.current space under the class name
  7. # of the module.
  8. 1 class Module
  9. # Defines a per-thread class attribute and creates class and instance reader methods.
  10. # The underlying per-thread class variable is set to +nil+, if it is not previously defined.
  11. #
  12. # module Current
  13. # thread_mattr_reader :user
  14. # end
  15. #
  16. # Current.user # => nil
  17. # Thread.current[:attr_Current_user] = "DHH"
  18. # Current.user # => "DHH"
  19. #
  20. # The attribute name must be a valid method name in Ruby.
  21. #
  22. # module Foo
  23. # thread_mattr_reader :"1_Badname"
  24. # end
  25. # # => NameError: invalid attribute name: 1_Badname
  26. #
  27. # To omit the instance reader method, pass
  28. # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
  29. #
  30. # class Current
  31. # thread_mattr_reader :user, instance_reader: false
  32. # end
  33. #
  34. # Current.new.user # => NoMethodError
  35. 1 def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
  36. 4 syms.each do |sym|
  37. 4 raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
  38. # The following generated method concatenates `name` because we want it
  39. # to work with inheritance via polymorphism.
  40. 4 class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  41. def self.#{sym}
  42. Thread.current["attr_" + name + "_#{sym}"]
  43. end
  44. EOS
  45. 4 if instance_reader && instance_accessor
  46. 2 class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  47. def #{sym}
  48. self.class.#{sym}
  49. end
  50. EOS
  51. end
  52. 4 Thread.current["attr_" + name + "_#{sym}"] = default unless default.nil?
  53. end
  54. end
  55. 1 alias :thread_cattr_reader :thread_mattr_reader
  56. # Defines a per-thread class attribute and creates a class and instance writer methods to
  57. # allow assignment to the attribute.
  58. #
  59. # module Current
  60. # thread_mattr_writer :user
  61. # end
  62. #
  63. # Current.user = "DHH"
  64. # Thread.current[:attr_Current_user] # => "DHH"
  65. #
  66. # To omit the instance writer method, pass
  67. # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
  68. #
  69. # class Current
  70. # thread_mattr_writer :user, instance_writer: false
  71. # end
  72. #
  73. # Current.new.user = "DHH" # => NoMethodError
  74. 1 def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
  75. 3 syms.each do |sym|
  76. 3 raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
  77. # The following generated method concatenates `name` because we want it
  78. # to work with inheritance via polymorphism.
  79. 3 class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  80. def self.#{sym}=(obj)
  81. Thread.current["attr_" + name + "_#{sym}"] = obj
  82. end
  83. EOS
  84. 3 if instance_writer && instance_accessor
  85. 1 class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  86. def #{sym}=(obj)
  87. self.class.#{sym} = obj
  88. end
  89. EOS
  90. end
  91. 3 public_send("#{sym}=", default) unless default.nil?
  92. end
  93. end
  94. 1 alias :thread_cattr_writer :thread_mattr_writer
  95. # Defines both class and instance accessors for class attributes.
  96. #
  97. # class Account
  98. # thread_mattr_accessor :user
  99. # end
  100. #
  101. # Account.user = "DHH"
  102. # Account.user # => "DHH"
  103. # Account.new.user # => "DHH"
  104. #
  105. # If a subclass changes the value, the parent class' value is not changed.
  106. # Similarly, if the parent class changes the value, the value of subclasses
  107. # is not changed.
  108. #
  109. # class Customer < Account
  110. # end
  111. #
  112. # Customer.user = "Rafael"
  113. # Customer.user # => "Rafael"
  114. # Account.user # => "DHH"
  115. #
  116. # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
  117. # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
  118. #
  119. # class Current
  120. # thread_mattr_accessor :user, instance_writer: false, instance_reader: false
  121. # end
  122. #
  123. # Current.new.user = "DHH" # => NoMethodError
  124. # Current.new.user # => NoMethodError
  125. #
  126. # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
  127. #
  128. # class Current
  129. # thread_mattr_accessor :user, instance_accessor: false
  130. # end
  131. #
  132. # Current.new.user = "DHH" # => NoMethodError
  133. # Current.new.user # => NoMethodError
  134. 1 def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
  135. 3 thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
  136. 3 thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
  137. end
  138. 1 alias :thread_cattr_accessor :thread_mattr_accessor
  139. end

lib/active_support/core_ext/module/concerning.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/concern"
  3. 1 class Module
  4. # = Bite-sized separation of concerns
  5. #
  6. # We often find ourselves with a medium-sized chunk of behavior that we'd
  7. # like to extract, but only mix in to a single class.
  8. #
  9. # Extracting a plain old Ruby object to encapsulate it and collaborate or
  10. # delegate to the original object is often a good choice, but when there's
  11. # no additional state to encapsulate or we're making DSL-style declarations
  12. # about the parent class, introducing new collaborators can obfuscate rather
  13. # than simplify.
  14. #
  15. # The typical route is to just dump everything in a monolithic class, perhaps
  16. # with a comment, as a least-bad alternative. Using modules in separate files
  17. # means tedious sifting to get a big-picture view.
  18. #
  19. # = Dissatisfying ways to separate small concerns
  20. #
  21. # == Using comments:
  22. #
  23. # class Todo < ApplicationRecord
  24. # # Other todo implementation
  25. # # ...
  26. #
  27. # ## Event tracking
  28. # has_many :events
  29. #
  30. # before_create :track_creation
  31. #
  32. # private
  33. # def track_creation
  34. # # ...
  35. # end
  36. # end
  37. #
  38. # == With an inline module:
  39. #
  40. # Noisy syntax.
  41. #
  42. # class Todo < ApplicationRecord
  43. # # Other todo implementation
  44. # # ...
  45. #
  46. # module EventTracking
  47. # extend ActiveSupport::Concern
  48. #
  49. # included do
  50. # has_many :events
  51. # before_create :track_creation
  52. # end
  53. #
  54. # private
  55. # def track_creation
  56. # # ...
  57. # end
  58. # end
  59. # include EventTracking
  60. # end
  61. #
  62. # == Mix-in noise exiled to its own file:
  63. #
  64. # Once our chunk of behavior starts pushing the scroll-to-understand-it
  65. # boundary, we give in and move it to a separate file. At this size, the
  66. # increased overhead can be a reasonable tradeoff even if it reduces our
  67. # at-a-glance perception of how things work.
  68. #
  69. # class Todo < ApplicationRecord
  70. # # Other todo implementation
  71. # # ...
  72. #
  73. # include TodoEventTracking
  74. # end
  75. #
  76. # = Introducing Module#concerning
  77. #
  78. # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
  79. # separate bite-sized concerns.
  80. #
  81. # class Todo < ApplicationRecord
  82. # # Other todo implementation
  83. # # ...
  84. #
  85. # concerning :EventTracking do
  86. # included do
  87. # has_many :events
  88. # before_create :track_creation
  89. # end
  90. #
  91. # private
  92. # def track_creation
  93. # # ...
  94. # end
  95. # end
  96. # end
  97. #
  98. # Todo.ancestors
  99. # # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
  100. #
  101. # This small step has some wonderful ripple effects. We can
  102. # * grok the behavior of our class in one glance,
  103. # * clean up monolithic junk-drawer classes by separating their concerns, and
  104. # * stop leaning on protected/private for crude "this is internal stuff" modularity.
  105. #
  106. # === Prepending `concerning`
  107. #
  108. # `concerning` supports a `prepend: true` argument which will `prepend` the
  109. # concern instead of using `include` for it.
  110. 1 module Concerning
  111. # Define a new concern and mix it in.
  112. 1 def concerning(topic, prepend: false, &block)
  113. 2 method = prepend ? :prepend : :include
  114. 2 __send__(method, concern(topic, &block))
  115. end
  116. # A low-cruft shortcut to define a concern.
  117. #
  118. # concern :EventTracking do
  119. # ...
  120. # end
  121. #
  122. # is equivalent to
  123. #
  124. # module EventTracking
  125. # extend ActiveSupport::Concern
  126. #
  127. # ...
  128. # end
  129. 1 def concern(topic, &module_definition)
  130. 2 const_set topic, Module.new {
  131. 2 extend ::ActiveSupport::Concern
  132. 2 module_eval(&module_definition)
  133. }
  134. end
  135. end
  136. 1 include Concerning
  137. end

lib/active_support/core_ext/module/delegation.rb

92.86% lines covered

42 relevant lines. 39 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "set"
  3. 24 class Module
  4. # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  5. # option is not used.
  6. 24 class DelegationError < NoMethodError; end
  7. 24 RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
  8. else elsif END end ensure false for if in module next nil not or redo rescue retry
  9. return self super then true undef unless until when while yield)
  10. 24 DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
  11. 24 DELEGATION_RESERVED_METHOD_NAMES = Set.new(
  12. RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  13. ).freeze
  14. # Provides a +delegate+ class method to easily expose contained objects'
  15. # public methods as your own.
  16. #
  17. # ==== Options
  18. # * <tt>:to</tt> - Specifies the target object name as a symbol or string
  19. # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
  20. # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
  21. # from being raised
  22. # * <tt>:private</tt> - If set to true, changes method visibility to private
  23. #
  24. # The macro receives one or more method names (specified as symbols or
  25. # strings) and the name of the target object via the <tt>:to</tt> option
  26. # (also a symbol or string).
  27. #
  28. # Delegation is particularly useful with Active Record associations:
  29. #
  30. # class Greeter < ActiveRecord::Base
  31. # def hello
  32. # 'hello'
  33. # end
  34. #
  35. # def goodbye
  36. # 'goodbye'
  37. # end
  38. # end
  39. #
  40. # class Foo < ActiveRecord::Base
  41. # belongs_to :greeter
  42. # delegate :hello, to: :greeter
  43. # end
  44. #
  45. # Foo.new.hello # => "hello"
  46. # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
  47. #
  48. # Multiple delegates to the same target are allowed:
  49. #
  50. # class Foo < ActiveRecord::Base
  51. # belongs_to :greeter
  52. # delegate :hello, :goodbye, to: :greeter
  53. # end
  54. #
  55. # Foo.new.goodbye # => "goodbye"
  56. #
  57. # Methods can be delegated to instance variables, class variables, or constants
  58. # by providing them as a symbols:
  59. #
  60. # class Foo
  61. # CONSTANT_ARRAY = [0,1,2,3]
  62. # @@class_array = [4,5,6,7]
  63. #
  64. # def initialize
  65. # @instance_array = [8,9,10,11]
  66. # end
  67. # delegate :sum, to: :CONSTANT_ARRAY
  68. # delegate :min, to: :@@class_array
  69. # delegate :max, to: :@instance_array
  70. # end
  71. #
  72. # Foo.new.sum # => 6
  73. # Foo.new.min # => 4
  74. # Foo.new.max # => 11
  75. #
  76. # It's also possible to delegate a method to the class by using +:class+:
  77. #
  78. # class Foo
  79. # def self.hello
  80. # "world"
  81. # end
  82. #
  83. # delegate :hello, to: :class
  84. # end
  85. #
  86. # Foo.new.hello # => "world"
  87. #
  88. # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
  89. # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
  90. # delegated to.
  91. #
  92. # Person = Struct.new(:name, :address)
  93. #
  94. # class Invoice < Struct.new(:client)
  95. # delegate :name, :address, to: :client, prefix: true
  96. # end
  97. #
  98. # john_doe = Person.new('John Doe', 'Vimmersvej 13')
  99. # invoice = Invoice.new(john_doe)
  100. # invoice.client_name # => "John Doe"
  101. # invoice.client_address # => "Vimmersvej 13"
  102. #
  103. # It is also possible to supply a custom prefix.
  104. #
  105. # class Invoice < Struct.new(:client)
  106. # delegate :name, :address, to: :client, prefix: :customer
  107. # end
  108. #
  109. # invoice = Invoice.new(john_doe)
  110. # invoice.customer_name # => 'John Doe'
  111. # invoice.customer_address # => 'Vimmersvej 13'
  112. #
  113. # The delegated methods are public by default.
  114. # Pass <tt>private: true</tt> to change that.
  115. #
  116. # class User < ActiveRecord::Base
  117. # has_one :profile
  118. # delegate :first_name, to: :profile
  119. # delegate :date_of_birth, to: :profile, private: true
  120. #
  121. # def age
  122. # Date.today.year - date_of_birth.year
  123. # end
  124. # end
  125. #
  126. # User.new.first_name # => "Tomas"
  127. # User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
  128. # User.new.age # => 2
  129. #
  130. # If the target is +nil+ and does not respond to the delegated method a
  131. # +Module::DelegationError+ is raised. If you wish to instead return +nil+,
  132. # use the <tt>:allow_nil</tt> option.
  133. #
  134. # class User < ActiveRecord::Base
  135. # has_one :profile
  136. # delegate :age, to: :profile
  137. # end
  138. #
  139. # User.new.age
  140. # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
  141. #
  142. # But if not having a profile yet is fine and should not be an error
  143. # condition:
  144. #
  145. # class User < ActiveRecord::Base
  146. # has_one :profile
  147. # delegate :age, to: :profile, allow_nil: true
  148. # end
  149. #
  150. # User.new.age # nil
  151. #
  152. # Note that if the target is not +nil+ then the call is attempted regardless of the
  153. # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
  154. # does not respond to the method:
  155. #
  156. # class Foo
  157. # def initialize(bar)
  158. # @bar = bar
  159. # end
  160. #
  161. # delegate :name, to: :@bar, allow_nil: true
  162. # end
  163. #
  164. # Foo.new("Bar").name # raises NoMethodError: undefined method `name'
  165. #
  166. # The target method must be public, otherwise it will raise +NoMethodError+.
  167. 24 def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
  168. 558 unless to
  169. raise ArgumentError, "Delegation needs a target. Supply a keyword argument 'to' (e.g. delegate :hello, to: :greeter)."
  170. end
  171. 558 if prefix == true && /^[^a-z_]/.match?(to)
  172. raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
  173. end
  174. 558 method_prefix = \
  175. 558 if prefix
  176. 6 "#{prefix == true ? to : prefix}_"
  177. else
  178. 552 ""
  179. end
  180. 558 location = caller_locations(1, 1).first
  181. 558 file, line = location.path, location.lineno
  182. 558 to = to.to_s
  183. 558 to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
  184. 558 method_def = []
  185. 558 method_names = []
  186. 558 methods.map do |method|
  187. 699 method_name = prefix ? "#{method_prefix}#{method}" : method
  188. 699 method_names << method_name.to_sym
  189. # Attribute writer methods only accept one argument. Makes sure []=
  190. # methods still accept two arguments.
  191. 699 definition = if /[^\]]=$/.match?(method)
  192. 170 "arg"
  193. 529 elsif RUBY_VERSION >= "2.7"
  194. "..."
  195. else
  196. 529 "*args, &block"
  197. end
  198. # The following generated method calls the target exactly once, storing
  199. # the returned value in a dummy variable.
  200. #
  201. # Reason is twofold: On one hand doing less calls is in general better.
  202. # On the other hand it could be that the target has side-effects,
  203. # whereas conceptually, from the user point of view, the delegator should
  204. # be doing one call.
  205. 699 if allow_nil
  206. 4 method = method.to_s
  207. method_def <<
  208. "def #{method_name}(#{definition})" <<
  209. " _ = #{to}" <<
  210. " if !_.nil? || nil.respond_to?(:#{method})" <<
  211. " _.#{method}(#{definition})" <<
  212. 4 " end" <<
  213. "end"
  214. else
  215. 695 method = method.to_s
  216. 695 method_name = method_name.to_s
  217. method_def <<
  218. "def #{method_name}(#{definition})" <<
  219. " _ = #{to}" <<
  220. " _.#{method}(#{definition})" <<
  221. "rescue NoMethodError => e" <<
  222. " if _.nil? && e.name == :#{method}" <<
  223. %( raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") <<
  224. " else" <<
  225. " raise" <<
  226. 695 " end" <<
  227. "end"
  228. end
  229. end
  230. 558 module_eval(method_def.join(";"), file, line)
  231. 558 private(*method_names) if private
  232. 558 method_names
  233. end
  234. # When building decorators, a common pattern may emerge:
  235. #
  236. # class Partition
  237. # def initialize(event)
  238. # @event = event
  239. # end
  240. #
  241. # def person
  242. # detail.person || creator
  243. # end
  244. #
  245. # private
  246. # def respond_to_missing?(name, include_private = false)
  247. # @event.respond_to?(name, include_private)
  248. # end
  249. #
  250. # def method_missing(method, *args, &block)
  251. # @event.send(method, *args, &block)
  252. # end
  253. # end
  254. #
  255. # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
  256. #
  257. # class Partition
  258. # delegate_missing_to :@event
  259. #
  260. # def initialize(event)
  261. # @event = event
  262. # end
  263. #
  264. # def person
  265. # detail.person || creator
  266. # end
  267. # end
  268. #
  269. # The target can be anything callable within the object, e.g. instance
  270. # variables, methods, constants, etc.
  271. #
  272. # The delegated method must be public on the target, otherwise it will
  273. # raise +DelegationError+. If you wish to instead return +nil+,
  274. # use the <tt>:allow_nil</tt> option.
  275. #
  276. # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
  277. # delegation due to possible interference when calling
  278. # <tt>Marshal.dump(object)</tt>, should the delegation target method
  279. # of <tt>object</tt> add or remove instance variables.
  280. 24 def delegate_missing_to(target, allow_nil: nil)
  281. 6 target = target.to_s
  282. 6 target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
  283. 6 module_eval <<-RUBY, __FILE__, __LINE__ + 1
  284. def respond_to_missing?(name, include_private = false)
  285. # It may look like an oversight, but we deliberately do not pass
  286. # +include_private+, because they do not get delegated.
  287. return false if name == :marshal_dump || name == :_dump
  288. #{target}.respond_to?(name) || super
  289. end
  290. def method_missing(method, *args, &block)
  291. if #{target}.respond_to?(method)
  292. #{target}.public_send(method, *args, &block)
  293. else
  294. begin
  295. super
  296. rescue NoMethodError
  297. if #{target}.nil?
  298. if #{allow_nil == true}
  299. nil
  300. else
  301. raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
  302. end
  303. else
  304. raise
  305. end
  306. end
  307. end
  308. end
  309. ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
  310. RUBY
  311. end
  312. end

lib/active_support/core_ext/module/deprecation.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 class Module
  3. # deprecate :foo
  4. # deprecate bar: 'message'
  5. # deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
  6. #
  7. # You can also use custom deprecator instance:
  8. #
  9. # deprecate :foo, deprecator: MyLib::Deprecator.new
  10. # deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
  11. #
  12. # \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
  13. # method where you can implement your custom warning behavior.
  14. #
  15. # class MyLib::Deprecator
  16. # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
  17. # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
  18. # Kernel.warn message
  19. # end
  20. # end
  21. 23 def deprecate(*method_names)
  22. 4 ActiveSupport::Deprecation.deprecate_methods(self, *method_names)
  23. end
  24. end

lib/active_support/core_ext/module/introspection.rb

31.03% lines covered

29 relevant lines. 9 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/core_ext/string/filters"
  3. 3 require "active_support/inflector"
  4. 3 class Module
  5. # Returns the name of the module containing this one.
  6. #
  7. # M::N.module_parent_name # => "M"
  8. 3 def module_parent_name
  9. if defined?(@parent_name)
  10. @parent_name
  11. else
  12. parent_name = name =~ /::[^:]+\z/ ? -$` : nil
  13. @parent_name = parent_name unless frozen?
  14. parent_name
  15. end
  16. end
  17. 3 def parent_name
  18. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  19. `Module#parent_name` has been renamed to `module_parent_name`.
  20. `parent_name` is deprecated and will be removed in Rails 6.1.
  21. MSG
  22. module_parent_name
  23. end
  24. # Returns the module which contains this one according to its name.
  25. #
  26. # module M
  27. # module N
  28. # end
  29. # end
  30. # X = M::N
  31. #
  32. # M::N.module_parent # => M
  33. # X.module_parent # => M
  34. #
  35. # The parent of top-level and anonymous modules is Object.
  36. #
  37. # M.module_parent # => Object
  38. # Module.new.module_parent # => Object
  39. 3 def module_parent
  40. module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object
  41. end
  42. 3 def parent
  43. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  44. `Module#parent` has been renamed to `module_parent`.
  45. `parent` is deprecated and will be removed in Rails 6.1.
  46. MSG
  47. module_parent
  48. end
  49. # Returns all the parents of this module according to its name, ordered from
  50. # nested outwards. The receiver is not contained within the result.
  51. #
  52. # module M
  53. # module N
  54. # end
  55. # end
  56. # X = M::N
  57. #
  58. # M.module_parents # => [Object]
  59. # M::N.module_parents # => [M, Object]
  60. # X.module_parents # => [M, Object]
  61. 3 def module_parents
  62. parents = []
  63. if module_parent_name
  64. parts = module_parent_name.split("::")
  65. until parts.empty?
  66. parents << ActiveSupport::Inflector.constantize(parts * "::")
  67. parts.pop
  68. end
  69. end
  70. parents << Object unless parents.include? Object
  71. parents
  72. end
  73. 3 def parents
  74. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  75. `Module#parents` has been renamed to `module_parents`.
  76. `parents` is deprecated and will be removed in Rails 6.1.
  77. MSG
  78. module_parents
  79. end
  80. end

lib/active_support/core_ext/module/reachable.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/module/anonymous"
  3. require "active_support/core_ext/string/inflections"
  4. ActiveSupport::Deprecation.warn("reachable is deprecated and will be removed from the framework.")

lib/active_support/core_ext/module/redefine_method.rb

87.5% lines covered

16 relevant lines. 14 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 class Module
  3. # Marks the named method as intended to be redefined, if it exists.
  4. # Suppresses the Ruby method redefinition warning. Prefer
  5. # #redefine_method where possible.
  6. 23 def silence_redefinition_of_method(method)
  7. 966 if method_defined?(method) || private_method_defined?(method)
  8. # This suppresses the "method redefined" warning; the self-alias
  9. # looks odd, but means we don't need to generate a unique name
  10. 916 alias_method method, method
  11. end
  12. end
  13. # Replaces the existing method definition, if there is one, with the passed
  14. # block as its body.
  15. 23 def redefine_method(method, &block)
  16. 340 visibility = method_visibility(method)
  17. 340 silence_redefinition_of_method(method)
  18. 340 define_method(method, &block)
  19. 340 send(visibility, method)
  20. end
  21. # Replaces the existing singleton method definition, if there is one, with
  22. # the passed block as its body.
  23. 23 def redefine_singleton_method(method, &block)
  24. 332 singleton_class.redefine_method(method, &block)
  25. end
  26. 23 def method_visibility(method) # :nodoc:
  27. case
  28. when private_method_defined?(method)
  29. :private
  30. when protected_method_defined?(method)
  31. :protected
  32. else
  33. 340 :public
  34. 340 end
  35. end
  36. end

lib/active_support/core_ext/module/remove_method.rb

57.14% lines covered

7 relevant lines. 4 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/redefine_method"
  3. 1 class Module
  4. # Removes the named method, if it exists.
  5. 1 def remove_possible_method(method)
  6. if method_defined?(method) || private_method_defined?(method)
  7. undef_method(method)
  8. end
  9. end
  10. # Removes the named singleton method, if it exists.
  11. 1 def remove_possible_singleton_method(method)
  12. singleton_class.remove_possible_method(method)
  13. end
  14. end

lib/active_support/core_ext/name_error.rb

32.0% lines covered

25 relevant lines. 8 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class NameError
  3. # Extract the name of the missing constant from the exception message.
  4. #
  5. # begin
  6. # HelloWorld
  7. # rescue NameError => e
  8. # e.missing_name
  9. # end
  10. # # => "HelloWorld"
  11. 3 def missing_name
  12. # Since ruby v2.3.0 `did_you_mean` gem is loaded by default.
  13. # It extends NameError#message with spell corrections which are SLOW.
  14. # We should use original_message message instead.
  15. message = respond_to?(:original_message) ? original_message : self.message
  16. return unless message.start_with?("uninitialized constant ")
  17. receiver = begin
  18. self.receiver
  19. rescue ArgumentError
  20. nil
  21. end
  22. if receiver == Object
  23. name.to_s
  24. elsif receiver
  25. "#{real_mod_name(receiver)}::#{self.name}"
  26. else
  27. if match = message.match(/((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/)
  28. match[1]
  29. end
  30. end
  31. end
  32. # Was this exception raised because the given name was missing?
  33. #
  34. # begin
  35. # HelloWorld
  36. # rescue NameError => e
  37. # e.missing_name?("HelloWorld")
  38. # end
  39. # # => true
  40. 3 def missing_name?(name)
  41. if name.is_a? Symbol
  42. self.name == name
  43. else
  44. missing_name == name.to_s
  45. end
  46. end
  47. 3 private
  48. 3 UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
  49. 3 private_constant :UNBOUND_METHOD_MODULE_NAME
  50. 3 if UnboundMethod.method_defined?(:bind_call)
  51. def real_mod_name(mod)
  52. UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
  53. end
  54. else
  55. 3 def real_mod_name(mod)
  56. UNBOUND_METHOD_MODULE_NAME.bind(mod).call
  57. end
  58. end
  59. end

lib/active_support/core_ext/numeric.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/numeric/bytes"
  3. 1 require "active_support/core_ext/numeric/time"
  4. 1 require "active_support/core_ext/numeric/conversions"

lib/active_support/core_ext/numeric/bytes.rb

78.57% lines covered

28 relevant lines. 22 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 13 class Numeric
  3. 13 KILOBYTE = 1024
  4. 13 MEGABYTE = KILOBYTE * 1024
  5. 13 GIGABYTE = MEGABYTE * 1024
  6. 13 TERABYTE = GIGABYTE * 1024
  7. 13 PETABYTE = TERABYTE * 1024
  8. 13 EXABYTE = PETABYTE * 1024
  9. # Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes
  10. #
  11. # 2.bytes # => 2
  12. 13 def bytes
  13. self
  14. end
  15. 13 alias :byte :bytes
  16. # Returns the number of bytes equivalent to the kilobytes provided.
  17. #
  18. # 2.kilobytes # => 2048
  19. 13 def kilobytes
  20. 20 self * KILOBYTE
  21. end
  22. 13 alias :kilobyte :kilobytes
  23. # Returns the number of bytes equivalent to the megabytes provided.
  24. #
  25. # 2.megabytes # => 2_097_152
  26. 13 def megabytes
  27. self * MEGABYTE
  28. end
  29. 13 alias :megabyte :megabytes
  30. # Returns the number of bytes equivalent to the gigabytes provided.
  31. #
  32. # 2.gigabytes # => 2_147_483_648
  33. 13 def gigabytes
  34. self * GIGABYTE
  35. end
  36. 13 alias :gigabyte :gigabytes
  37. # Returns the number of bytes equivalent to the terabytes provided.
  38. #
  39. # 2.terabytes # => 2_199_023_255_552
  40. 13 def terabytes
  41. self * TERABYTE
  42. end
  43. 13 alias :terabyte :terabytes
  44. # Returns the number of bytes equivalent to the petabytes provided.
  45. #
  46. # 2.petabytes # => 2_251_799_813_685_248
  47. 13 def petabytes
  48. self * PETABYTE
  49. end
  50. 13 alias :petabyte :petabytes
  51. # Returns the number of bytes equivalent to the exabytes provided.
  52. #
  53. # 2.exabytes # => 2_305_843_009_213_693_952
  54. 13 def exabytes
  55. self * EXABYTE
  56. end
  57. 13 alias :exabyte :exabytes
  58. end

lib/active_support/core_ext/numeric/conversions.rb

52.38% lines covered

21 relevant lines. 11 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/big_decimal/conversions"
  3. 1 require "active_support/number_helper"
  4. 1 require "active_support/core_ext/module/deprecation"
  5. 1 module ActiveSupport
  6. 1 module NumericWithFormat
  7. # Provides options for converting numbers into formatted strings.
  8. # Options are provided for phone numbers, currency, percentage,
  9. # precision, positional notation, file size and pretty printing.
  10. #
  11. # ==== Options
  12. #
  13. # For details on which formats use which options, see ActiveSupport::NumberHelper
  14. #
  15. # ==== Examples
  16. #
  17. # Phone Numbers:
  18. # 5551234.to_s(:phone) # => "555-1234"
  19. # 1235551234.to_s(:phone) # => "123-555-1234"
  20. # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
  21. # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
  22. # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
  23. # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
  24. # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
  25. # # => "+1.123.555.1234 x 1343"
  26. #
  27. # Currency:
  28. # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
  29. # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
  30. # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
  31. # 1234567890.506.to_s(:currency, round_mode: :down) # => "$1,234,567,890.50"
  32. # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
  33. # -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
  34. # # => "($1,234,567,890.50)"
  35. # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '')
  36. # # => "&pound;1234567890,50"
  37. # 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
  38. # # => "1234567890,50 &pound;"
  39. #
  40. # Percentage:
  41. # 100.to_s(:percentage) # => "100.000%"
  42. # 100.to_s(:percentage, precision: 0) # => "100%"
  43. # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
  44. # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
  45. # 302.24398923423.to_s(:percentage, round_mode: :down) # => "302.243%"
  46. # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
  47. # 100.to_s(:percentage, format: '%n %') # => "100.000 %"
  48. #
  49. # Delimited:
  50. # 12345678.to_s(:delimited) # => "12,345,678"
  51. # 12345678.05.to_s(:delimited) # => "12,345,678.05"
  52. # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
  53. # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
  54. # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
  55. # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
  56. # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
  57. # # => "98 765 432,98"
  58. #
  59. # Rounded:
  60. # 111.2345.to_s(:rounded) # => "111.235"
  61. # 111.2345.to_s(:rounded, precision: 2) # => "111.23"
  62. # 111.2345.to_s(:rounded, precision: 2, round_mode: :up) # => "111.24"
  63. # 13.to_s(:rounded, precision: 5) # => "13.00000"
  64. # 389.32314.to_s(:rounded, precision: 0) # => "389"
  65. # 111.2345.to_s(:rounded, significant: true) # => "111"
  66. # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
  67. # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
  68. # 111.234.to_s(:rounded, locale: :fr) # => "111,234"
  69. # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
  70. # # => "13"
  71. # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
  72. # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
  73. # # => "1.111,23"
  74. #
  75. # Human-friendly size in Bytes:
  76. # 123.to_s(:human_size) # => "123 Bytes"
  77. # 1234.to_s(:human_size) # => "1.21 KB"
  78. # 12345.to_s(:human_size) # => "12.1 KB"
  79. # 1234567.to_s(:human_size) # => "1.18 MB"
  80. # 1234567890.to_s(:human_size) # => "1.15 GB"
  81. # 1234567890123.to_s(:human_size) # => "1.12 TB"
  82. # 1234567890123456.to_s(:human_size) # => "1.1 PB"
  83. # 1234567890123456789.to_s(:human_size) # => "1.07 EB"
  84. # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
  85. # 1234567.to_s(:human_size, precision: 2, round_mode: :up) # => "1.3 MB"
  86. # 483989.to_s(:human_size, precision: 2) # => "470 KB"
  87. # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
  88. # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
  89. # 524288000.to_s(:human_size, precision: 5) # => "500 MB"
  90. #
  91. # Human-friendly format:
  92. # 123.to_s(:human) # => "123"
  93. # 1234.to_s(:human) # => "1.23 Thousand"
  94. # 12345.to_s(:human) # => "12.3 Thousand"
  95. # 1234567.to_s(:human) # => "1.23 Million"
  96. # 1234567890.to_s(:human) # => "1.23 Billion"
  97. # 1234567890123.to_s(:human) # => "1.23 Trillion"
  98. # 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
  99. # 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
  100. # 489939.to_s(:human, precision: 2) # => "490 Thousand"
  101. # 489939.to_s(:human, precision: 2, round_mode: :down) # => "480 Thousand"
  102. # 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
  103. # 1234567.to_s(:human, precision: 4,
  104. # significant: false) # => "1.2346 Million"
  105. # 1234567.to_s(:human, precision: 1,
  106. # separator: ',',
  107. # significant: false) # => "1,2 Million"
  108. 1 def to_s(format = nil, options = nil)
  109. 4699 case format
  110. when nil
  111. 4699 super()
  112. when Integer, String
  113. super(format)
  114. when :phone
  115. ActiveSupport::NumberHelper.number_to_phone(self, options || {})
  116. when :currency
  117. ActiveSupport::NumberHelper.number_to_currency(self, options || {})
  118. when :percentage
  119. ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
  120. when :delimited
  121. ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
  122. when :rounded
  123. ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
  124. when :human
  125. ActiveSupport::NumberHelper.number_to_human(self, options || {})
  126. when :human_size
  127. ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
  128. when Symbol
  129. super()
  130. else
  131. super(format)
  132. end
  133. end
  134. end
  135. end
  136. 1 Integer.prepend ActiveSupport::NumericWithFormat
  137. 1 Float.prepend ActiveSupport::NumericWithFormat
  138. 1 BigDecimal.prepend ActiveSupport::NumericWithFormat

lib/active_support/core_ext/numeric/inquiry.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation"
  3. ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Numeric#positive? and Numeric#negative? natively, so requiring active_support/core_ext/numeric/inquiry is no longer necessary. Requiring it will raise LoadError in Rails 6.1."

lib/active_support/core_ext/numeric/time.rb

73.08% lines covered

26 relevant lines. 19 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 14 require "active_support/duration"
  3. 14 require "active_support/core_ext/time/calculations"
  4. 14 require "active_support/core_ext/time/acts_like"
  5. 14 require "active_support/core_ext/date/calculations"
  6. 14 require "active_support/core_ext/date/acts_like"
  7. 14 class Numeric
  8. # Returns a Duration instance matching the number of seconds provided.
  9. #
  10. # 2.seconds # => 2 seconds
  11. 14 def seconds
  12. ActiveSupport::Duration.seconds(self)
  13. end
  14. 14 alias :second :seconds
  15. # Returns a Duration instance matching the number of minutes provided.
  16. #
  17. # 2.minutes # => 2 minutes
  18. 14 def minutes
  19. ActiveSupport::Duration.minutes(self)
  20. end
  21. 14 alias :minute :minutes
  22. # Returns a Duration instance matching the number of hours provided.
  23. #
  24. # 2.hours # => 2 hours
  25. 14 def hours
  26. ActiveSupport::Duration.hours(self)
  27. end
  28. 14 alias :hour :hours
  29. # Returns a Duration instance matching the number of days provided.
  30. #
  31. # 2.days # => 2 days
  32. 14 def days
  33. ActiveSupport::Duration.days(self)
  34. end
  35. 14 alias :day :days
  36. # Returns a Duration instance matching the number of weeks provided.
  37. #
  38. # 2.weeks # => 2 weeks
  39. 14 def weeks
  40. ActiveSupport::Duration.weeks(self)
  41. end
  42. 14 alias :week :weeks
  43. # Returns a Duration instance matching the number of fortnights provided.
  44. #
  45. # 2.fortnights # => 4 weeks
  46. 14 def fortnights
  47. ActiveSupport::Duration.weeks(self * 2)
  48. end
  49. 14 alias :fortnight :fortnights
  50. # Returns the number of milliseconds equivalent to the seconds provided.
  51. # Used with the standard time durations.
  52. #
  53. # 2.in_milliseconds # => 2000
  54. # 1.hour.in_milliseconds # => 3600000
  55. 14 def in_milliseconds
  56. self * 1000
  57. end
  58. end

lib/active_support/core_ext/object.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/acts_like"
  3. 1 require "active_support/core_ext/object/blank"
  4. 1 require "active_support/core_ext/object/duplicable"
  5. 1 require "active_support/core_ext/object/deep_dup"
  6. 1 require "active_support/core_ext/object/try"
  7. 1 require "active_support/core_ext/object/inclusion"
  8. 1 require "active_support/core_ext/object/conversions"
  9. 1 require "active_support/core_ext/object/instance_variables"
  10. 1 require "active_support/core_ext/object/json"
  11. 1 require "active_support/core_ext/object/to_param"
  12. 1 require "active_support/core_ext/object/to_query"
  13. 1 require "active_support/core_ext/object/with_options"

lib/active_support/core_ext/object/acts_like.rb

28.57% lines covered

7 relevant lines. 2 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 class Object
  3. # A duck-type assistant method. For example, Active Support extends Date
  4. # to define an <tt>acts_like_date?</tt> method, and extends Time to define
  5. # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
  6. # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
  7. # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
  8. 23 def acts_like?(duck)
  9. case duck
  10. when :time
  11. respond_to? :acts_like_time?
  12. when :date
  13. respond_to? :acts_like_date?
  14. when :string
  15. respond_to? :acts_like_string?
  16. else
  17. respond_to? :"acts_like_#{duck}?"
  18. end
  19. end
  20. end

lib/active_support/core_ext/object/blank.rb

71.43% lines covered

35 relevant lines. 25 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "concurrent/map"
  3. 24 class Object
  4. # An object is blank if it's false, empty, or a whitespace string.
  5. # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
  6. #
  7. # This simplifies
  8. #
  9. # !address || address.empty?
  10. #
  11. # to
  12. #
  13. # address.blank?
  14. #
  15. # @return [true, false]
  16. 24 def blank?
  17. 34 respond_to?(:empty?) ? !!empty? : !self
  18. end
  19. # An object is present if it's not blank.
  20. #
  21. # @return [true, false]
  22. 24 def present?
  23. !blank?
  24. end
  25. # Returns the receiver if it's present otherwise returns +nil+.
  26. # <tt>object.presence</tt> is equivalent to
  27. #
  28. # object.present? ? object : nil
  29. #
  30. # For example, something like
  31. #
  32. # state = params[:state] if params[:state].present?
  33. # country = params[:country] if params[:country].present?
  34. # region = state || country || 'US'
  35. #
  36. # becomes
  37. #
  38. # region = params[:state].presence || params[:country].presence || 'US'
  39. #
  40. # @return [Object]
  41. 24 def presence
  42. self if present?
  43. end
  44. end
  45. 24 class NilClass
  46. # +nil+ is blank:
  47. #
  48. # nil.blank? # => true
  49. #
  50. # @return [true]
  51. 24 def blank?
  52. 248 true
  53. end
  54. end
  55. 24 class FalseClass
  56. # +false+ is blank:
  57. #
  58. # false.blank? # => true
  59. #
  60. # @return [true]
  61. 24 def blank?
  62. true
  63. end
  64. end
  65. 24 class TrueClass
  66. # +true+ is not blank:
  67. #
  68. # true.blank? # => false
  69. #
  70. # @return [false]
  71. 24 def blank?
  72. false
  73. end
  74. end
  75. 24 class Array
  76. # An array is blank if it's empty:
  77. #
  78. # [].blank? # => true
  79. # [1,2,3].blank? # => false
  80. #
  81. # @return [true, false]
  82. 24 alias_method :blank?, :empty?
  83. end
  84. 24 class Hash
  85. # A hash is blank if it's empty:
  86. #
  87. # {}.blank? # => true
  88. # { key: 'value' }.blank? # => false
  89. #
  90. # @return [true, false]
  91. 24 alias_method :blank?, :empty?
  92. end
  93. 24 class String
  94. 24 BLANK_RE = /\A[[:space:]]*\z/
  95. 24 ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
  96. h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
  97. end
  98. # A string is blank if it's empty or contains whitespaces only:
  99. #
  100. # ''.blank? # => true
  101. # ' '.blank? # => true
  102. # "\t\n\r".blank? # => true
  103. # ' blah '.blank? # => false
  104. #
  105. # Unicode whitespace is supported:
  106. #
  107. # "\u00a0".blank? # => true
  108. #
  109. # @return [true, false]
  110. 24 def blank?
  111. # The regexp that matches blank strings is expensive. For the case of empty
  112. # strings we can speed up this method (~3.5x) with an empty? call. The
  113. # penalty for the rest of strings is marginal.
  114. empty? ||
  115. begin
  116. BLANK_RE.match?(self)
  117. rescue Encoding::CompatibilityError
  118. ENCODED_BLANKS[self.encoding].match?(self)
  119. end
  120. end
  121. end
  122. 24 class Numeric #:nodoc:
  123. # No number is blank:
  124. #
  125. # 1.blank? # => false
  126. # 0.blank? # => false
  127. #
  128. # @return [false]
  129. 24 def blank?
  130. false
  131. end
  132. end
  133. 24 class Time #:nodoc:
  134. # No Time is blank:
  135. #
  136. # Time.now.blank? # => false
  137. #
  138. # @return [false]
  139. 24 def blank?
  140. false
  141. end
  142. end

lib/active_support/core_ext/object/conversions.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/to_param"
  3. 1 require "active_support/core_ext/object/to_query"
  4. 1 require "active_support/core_ext/array/conversions"
  5. 1 require "active_support/core_ext/hash/conversions"

lib/active_support/core_ext/object/deep_dup.rb

43.75% lines covered

16 relevant lines. 7 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/duplicable"
  3. 1 class Object
  4. # Returns a deep copy of object if it's duplicable. If it's
  5. # not duplicable, returns +self+.
  6. #
  7. # object = Object.new
  8. # dup = object.deep_dup
  9. # dup.instance_variable_set(:@a, 1)
  10. #
  11. # object.instance_variable_defined?(:@a) # => false
  12. # dup.instance_variable_defined?(:@a) # => true
  13. 1 def deep_dup
  14. duplicable? ? dup : self
  15. end
  16. end
  17. 1 class Array
  18. # Returns a deep copy of array.
  19. #
  20. # array = [1, [2, 3]]
  21. # dup = array.deep_dup
  22. # dup[1][2] = 4
  23. #
  24. # array[1][2] # => nil
  25. # dup[1][2] # => 4
  26. 1 def deep_dup
  27. map(&:deep_dup)
  28. end
  29. end
  30. 1 class Hash
  31. # Returns a deep copy of hash.
  32. #
  33. # hash = { a: { b: 'b' } }
  34. # dup = hash.deep_dup
  35. # dup[:a][:c] = 'c'
  36. #
  37. # hash[:a][:c] # => nil
  38. # dup[:a][:c] # => "c"
  39. 1 def deep_dup
  40. hash = dup
  41. each_pair do |key, value|
  42. if key.frozen? && ::String === key
  43. hash[key] = value.deep_dup
  44. else
  45. hash.delete(key)
  46. hash[key.deep_dup] = value.deep_dup
  47. end
  48. end
  49. hash
  50. end
  51. end

lib/active_support/core_ext/object/duplicable.rb

66.67% lines covered

9 relevant lines. 6 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Most objects are cloneable, but not all. For example you can't dup methods:
  4. #
  5. # method(:puts).dup # => TypeError: allocator undefined for Method
  6. #
  7. # Classes may signal their instances are not duplicable removing +dup+/+clone+
  8. # or raising exceptions from them. So, to dup an arbitrary object you normally
  9. # use an optimistic approach and are ready to catch an exception, say:
  10. #
  11. # arbitrary_object.dup rescue object
  12. #
  13. # Rails dups objects in a few critical spots where they are not that arbitrary.
  14. # That rescue is very expensive (like 40 times slower than a predicate), and it
  15. # is often triggered.
  16. #
  17. # That's why we hardcode the following cases and check duplicable? instead of
  18. # using that rescue idiom.
  19. #++
  20. 1 class Object
  21. # Can you safely dup this object?
  22. #
  23. # False for method objects;
  24. # true otherwise.
  25. 1 def duplicable?
  26. true
  27. end
  28. end
  29. 1 class Method
  30. # Methods are not duplicable:
  31. #
  32. # method(:puts).duplicable? # => false
  33. # method(:puts).dup # => TypeError: allocator undefined for Method
  34. 1 def duplicable?
  35. false
  36. end
  37. end
  38. 1 class UnboundMethod
  39. # Unbound methods are not duplicable:
  40. #
  41. # method(:puts).unbind.duplicable? # => false
  42. # method(:puts).unbind.dup # => TypeError: allocator undefined for UnboundMethod
  43. 1 def duplicable?
  44. false
  45. end
  46. end

lib/active_support/core_ext/object/inclusion.rb

50.0% lines covered

6 relevant lines. 3 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Object
  3. # Returns true if this object is included in the argument. Argument must be
  4. # any object which responds to +#include?+. Usage:
  5. #
  6. # characters = ["Konata", "Kagami", "Tsukasa"]
  7. # "Konata".in?(characters) # => true
  8. #
  9. # This will throw an +ArgumentError+ if the argument doesn't respond
  10. # to +#include?+.
  11. 2 def in?(another_object)
  12. another_object.include?(self)
  13. rescue NoMethodError
  14. raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
  15. end
  16. # Returns the receiver if it's included in the argument otherwise returns +nil+.
  17. # Argument must be any object which responds to +#include?+. Usage:
  18. #
  19. # params[:bucket_type].presence_in %w( project calendar )
  20. #
  21. # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+.
  22. #
  23. # @return [Object]
  24. 2 def presence_in(another_object)
  25. in?(another_object) ? self : nil
  26. end
  27. end

lib/active_support/core_ext/object/instance_variables.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Object
  3. # Returns a hash with string keys that maps instance variable names without "@" to their
  4. # corresponding values.
  5. #
  6. # class C
  7. # def initialize(x, y)
  8. # @x, @y = x, y
  9. # end
  10. # end
  11. #
  12. # C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
  13. 2 def instance_values
  14. Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }]
  15. end
  16. # Returns an array of instance variable names as strings including "@".
  17. #
  18. # class C
  19. # def initialize(x, y)
  20. # @x, @y = x, y
  21. # end
  22. # end
  23. #
  24. # C.new(0, 1).instance_variable_names # => ["@y", "@x"]
  25. 2 def instance_variable_names
  26. instance_variables.map(&:to_s)
  27. end
  28. end

lib/active_support/core_ext/object/json.rb

58.88% lines covered

107 relevant lines. 63 lines covered and 44 lines missed.
    
  1. # frozen_string_literal: true
  2. # Hack to load json gem first so we can overwrite its to_json.
  3. 2 require "json"
  4. 2 require "bigdecimal"
  5. 2 require "uri/generic"
  6. 2 require "pathname"
  7. 2 require "active_support/core_ext/big_decimal/conversions" # for #to_s
  8. 2 require "active_support/core_ext/hash/except"
  9. 2 require "active_support/core_ext/hash/slice"
  10. 2 require "active_support/core_ext/object/instance_variables"
  11. 2 require "time"
  12. 2 require "active_support/core_ext/time/conversions"
  13. 2 require "active_support/core_ext/date_time/conversions"
  14. 2 require "active_support/core_ext/date/conversions"
  15. #--
  16. # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
  17. # their default behavior. That said, we need to define the basic to_json method in all of them,
  18. # otherwise they will always use to_json gem implementation, which is backwards incompatible in
  19. # several cases (for instance, the JSON implementation for Hash does not work) with inheritance
  20. # and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
  21. #
  22. # On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
  23. # JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
  24. # passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
  25. # calls to the original to_json method.
  26. #
  27. # It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
  28. # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
  29. # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
  30. # should give exactly the same results with or without active support.
  31. 2 module ActiveSupport
  32. 2 module ToJsonWithActiveSupportEncoder # :nodoc:
  33. 2 def to_json(options = nil)
  34. if options.is_a?(::JSON::State)
  35. # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
  36. super(options)
  37. else
  38. # to_json is being invoked directly, use ActiveSupport's encoder
  39. ActiveSupport::JSON.encode(self, options)
  40. end
  41. end
  42. end
  43. end
  44. 2 [Enumerable, Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].reverse_each do |klass|
  45. 20 klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
  46. end
  47. 2 class Object
  48. 2 def as_json(options = nil) #:nodoc:
  49. if respond_to?(:to_hash)
  50. to_hash.as_json(options)
  51. else
  52. instance_values.as_json(options)
  53. end
  54. end
  55. end
  56. 2 class Struct #:nodoc:
  57. 2 def as_json(options = nil)
  58. Hash[members.zip(values)].as_json(options)
  59. end
  60. end
  61. 2 class TrueClass
  62. 2 def as_json(options = nil) #:nodoc:
  63. self
  64. end
  65. end
  66. 2 class FalseClass
  67. 2 def as_json(options = nil) #:nodoc:
  68. self
  69. end
  70. end
  71. 2 class NilClass
  72. 2 def as_json(options = nil) #:nodoc:
  73. self
  74. end
  75. end
  76. 2 class String
  77. 2 def as_json(options = nil) #:nodoc:
  78. self
  79. end
  80. end
  81. 2 class Symbol
  82. 2 def as_json(options = nil) #:nodoc:
  83. to_s
  84. end
  85. end
  86. 2 class Numeric
  87. 2 def as_json(options = nil) #:nodoc:
  88. self
  89. end
  90. end
  91. 2 class Float
  92. # Encoding Infinity or NaN to JSON should return "null". The default returns
  93. # "Infinity" or "NaN" which are not valid JSON.
  94. 2 def as_json(options = nil) #:nodoc:
  95. finite? ? self : nil
  96. end
  97. end
  98. 2 class BigDecimal
  99. # A BigDecimal would be naturally represented as a JSON number. Most libraries,
  100. # however, parse non-integer JSON numbers directly as floats. Clients using
  101. # those libraries would get in general a wrong number and no way to recover
  102. # other than manually inspecting the string with the JSON code itself.
  103. #
  104. # That's why a JSON string is returned. The JSON literal is not numeric, but
  105. # if the other end knows by contract that the data is supposed to be a
  106. # BigDecimal, it still has the chance to post-process the string and get the
  107. # real value.
  108. 2 def as_json(options = nil) #:nodoc:
  109. finite? ? to_s : nil
  110. end
  111. end
  112. 2 class Regexp
  113. 2 def as_json(options = nil) #:nodoc:
  114. to_s
  115. end
  116. end
  117. 2 module Enumerable
  118. 2 def as_json(options = nil) #:nodoc:
  119. to_a.as_json(options)
  120. end
  121. end
  122. 2 class IO
  123. 2 def as_json(options = nil) #:nodoc:
  124. to_s
  125. end
  126. end
  127. 2 class Range
  128. 2 def as_json(options = nil) #:nodoc:
  129. to_s
  130. end
  131. end
  132. 2 class Array
  133. 2 def as_json(options = nil) #:nodoc:
  134. map { |v| options ? v.as_json(options.dup) : v.as_json }
  135. end
  136. end
  137. 2 class Hash
  138. 2 def as_json(options = nil) #:nodoc:
  139. # create a subset of the hash by applying :only or :except
  140. subset = if options
  141. if attrs = options[:only]
  142. slice(*Array(attrs))
  143. elsif attrs = options[:except]
  144. except(*Array(attrs))
  145. else
  146. self
  147. end
  148. else
  149. self
  150. end
  151. result = {}
  152. subset.each do |k, v|
  153. result[k.to_s] = options ? v.as_json(options.dup) : v.as_json
  154. end
  155. result
  156. end
  157. end
  158. 2 class Time
  159. 2 def as_json(options = nil) #:nodoc:
  160. if ActiveSupport::JSON::Encoding.use_standard_json_time_format
  161. xmlschema(ActiveSupport::JSON::Encoding.time_precision)
  162. else
  163. %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
  164. end
  165. end
  166. end
  167. 2 class Date
  168. 2 def as_json(options = nil) #:nodoc:
  169. if ActiveSupport::JSON::Encoding.use_standard_json_time_format
  170. strftime("%Y-%m-%d")
  171. else
  172. strftime("%Y/%m/%d")
  173. end
  174. end
  175. end
  176. 2 class DateTime
  177. 2 def as_json(options = nil) #:nodoc:
  178. if ActiveSupport::JSON::Encoding.use_standard_json_time_format
  179. xmlschema(ActiveSupport::JSON::Encoding.time_precision)
  180. else
  181. strftime("%Y/%m/%d %H:%M:%S %z")
  182. end
  183. end
  184. end
  185. 2 class URI::Generic #:nodoc:
  186. 2 def as_json(options = nil)
  187. to_s
  188. end
  189. end
  190. 2 class Pathname #:nodoc:
  191. 2 def as_json(options = nil)
  192. to_s
  193. end
  194. end
  195. 2 class Process::Status #:nodoc:
  196. 2 def as_json(options = nil)
  197. { exitstatus: exitstatus, pid: pid }
  198. end
  199. end
  200. 2 class Exception
  201. 2 def as_json(options = nil)
  202. to_s
  203. end
  204. end

lib/active_support/core_ext/object/to_param.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/object/to_query"

lib/active_support/core_ext/object/to_query.rb

51.61% lines covered

31 relevant lines. 16 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "cgi"
  3. 23 class Object
  4. # Alias of <tt>to_s</tt>.
  5. 23 def to_param
  6. to_s
  7. end
  8. # Converts an object into a string suitable for use as a URL query string,
  9. # using the given <tt>key</tt> as the param name.
  10. 23 def to_query(key)
  11. "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
  12. end
  13. end
  14. 23 class NilClass
  15. # Returns +self+.
  16. 23 def to_param
  17. self
  18. end
  19. end
  20. 23 class TrueClass
  21. # Returns +self+.
  22. 23 def to_param
  23. self
  24. end
  25. end
  26. 23 class FalseClass
  27. # Returns +self+.
  28. 23 def to_param
  29. self
  30. end
  31. end
  32. 23 class Array
  33. # Calls <tt>to_param</tt> on all its elements and joins the result with
  34. # slashes. This is used by <tt>url_for</tt> in Action Pack.
  35. 23 def to_param
  36. collect(&:to_param).join "/"
  37. end
  38. # Converts an array into a string suitable for use as a URL query string,
  39. # using the given +key+ as the param name.
  40. #
  41. # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
  42. 23 def to_query(key)
  43. prefix = "#{key}[]"
  44. if empty?
  45. nil.to_query(prefix)
  46. else
  47. collect { |value| value.to_query(prefix) }.join "&"
  48. end
  49. end
  50. end
  51. 23 class Hash
  52. # Returns a string representation of the receiver suitable for use as a URL
  53. # query string:
  54. #
  55. # {name: 'David', nationality: 'Danish'}.to_query
  56. # # => "name=David&nationality=Danish"
  57. #
  58. # An optional namespace can be passed to enclose key names:
  59. #
  60. # {name: 'David', nationality: 'Danish'}.to_query('user')
  61. # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
  62. #
  63. # The string pairs "key=value" that conform the query string
  64. # are sorted lexicographically in ascending order.
  65. #
  66. # This method is also aliased as +to_param+.
  67. 23 def to_query(namespace = nil)
  68. query = collect do |key, value|
  69. unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
  70. value.to_query(namespace ? "#{namespace}[#{key}]" : key)
  71. end
  72. end.compact
  73. query.sort! unless namespace.to_s.include?("[]")
  74. query.join("&")
  75. end
  76. 23 alias_method :to_param, :to_query
  77. end

lib/active_support/core_ext/object/try.rb

56.0% lines covered

25 relevant lines. 14 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "delegate"
  3. 24 module ActiveSupport
  4. 24 module Tryable #:nodoc:
  5. 24 def try(method_name = nil, *args, &b)
  6. if method_name.nil? && block_given?
  7. if b.arity == 0
  8. instance_eval(&b)
  9. else
  10. yield self
  11. end
  12. elsif respond_to?(method_name)
  13. public_send(method_name, *args, &b)
  14. end
  15. end
  16. 24 ruby2_keywords(:try) if respond_to?(:ruby2_keywords, true)
  17. 24 def try!(method_name = nil, *args, &b)
  18. if method_name.nil? && block_given?
  19. if b.arity == 0
  20. instance_eval(&b)
  21. else
  22. yield self
  23. end
  24. else
  25. public_send(method_name, *args, &b)
  26. end
  27. end
  28. 24 ruby2_keywords(:try!) if respond_to?(:ruby2_keywords, true)
  29. end
  30. end
  31. 24 class Object
  32. 24 include ActiveSupport::Tryable
  33. ##
  34. # :method: try
  35. #
  36. # :call-seq:
  37. # try(*a, &b)
  38. #
  39. # Invokes the public method whose name goes as first argument just like
  40. # +public_send+ does, except that if the receiver does not respond to it the
  41. # call returns +nil+ rather than raising an exception.
  42. #
  43. # This method is defined to be able to write
  44. #
  45. # @person.try(:name)
  46. #
  47. # instead of
  48. #
  49. # @person.name if @person
  50. #
  51. # +try+ calls can be chained:
  52. #
  53. # @person.try(:spouse).try(:name)
  54. #
  55. # instead of
  56. #
  57. # @person.spouse.name if @person && @person.spouse
  58. #
  59. # +try+ will also return +nil+ if the receiver does not respond to the method:
  60. #
  61. # @person.try(:non_existing_method) # => nil
  62. #
  63. # instead of
  64. #
  65. # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
  66. #
  67. # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
  68. # to the method:
  69. #
  70. # nil.try(:to_i) # => nil, rather than 0
  71. #
  72. # Arguments and blocks are forwarded to the method if invoked:
  73. #
  74. # @posts.try(:each_slice, 2) do |a, b|
  75. # ...
  76. # end
  77. #
  78. # The number of arguments in the signature must match. If the object responds
  79. # to the method the call is attempted and +ArgumentError+ is still raised
  80. # in case of argument mismatch.
  81. #
  82. # If +try+ is called without arguments it yields the receiver to a given
  83. # block unless it is +nil+:
  84. #
  85. # @person.try do |p|
  86. # ...
  87. # end
  88. #
  89. # You can also call try with a block without accepting an argument, and the block
  90. # will be instance_eval'ed instead:
  91. #
  92. # @person.try { upcase.truncate(50) }
  93. #
  94. # Please also note that +try+ is defined on +Object+. Therefore, it won't work
  95. # with instances of classes that do not have +Object+ among their ancestors,
  96. # like direct subclasses of +BasicObject+.
  97. ##
  98. # :method: try!
  99. #
  100. # :call-seq:
  101. # try!(*a, &b)
  102. #
  103. # Same as #try, but raises a +NoMethodError+ exception if the receiver is
  104. # not +nil+ and does not implement the tried method.
  105. #
  106. # "a".try!(:upcase) # => "A"
  107. # nil.try!(:upcase) # => nil
  108. # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
  109. end
  110. 24 class Delegator
  111. 24 include ActiveSupport::Tryable
  112. ##
  113. # :method: try
  114. #
  115. # :call-seq:
  116. # try(a*, &b)
  117. #
  118. # See Object#try
  119. ##
  120. # :method: try!
  121. #
  122. # :call-seq:
  123. # try!(a*, &b)
  124. #
  125. # See Object#try!
  126. end
  127. 24 class NilClass
  128. # Calling +try+ on +nil+ always returns +nil+.
  129. # It becomes especially helpful when navigating through associations that may return +nil+.
  130. #
  131. # nil.try(:name) # => nil
  132. #
  133. # Without +try+
  134. # @person && @person.children.any? && @person.children.first.name
  135. #
  136. # With +try+
  137. # @person.try(:children).try(:first).try(:name)
  138. 24 def try(_method_name = nil, *)
  139. nil
  140. end
  141. # Calling +try!+ on +nil+ always returns +nil+.
  142. #
  143. # nil.try!(:name) # => nil
  144. 24 def try!(_method_name = nil, *)
  145. nil
  146. end
  147. end

lib/active_support/core_ext/object/with_options.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/option_merger"
  3. 2 class Object
  4. # An elegant way to factor duplication out of options passed to a series of
  5. # method calls. Each method called in the block, with the block variable as
  6. # the receiver, will have its options merged with the default +options+ hash
  7. # provided. Each method called on the block variable must take an options
  8. # hash as its final argument.
  9. #
  10. # Without <tt>with_options</tt>, this code contains duplication:
  11. #
  12. # class Account < ActiveRecord::Base
  13. # has_many :customers, dependent: :destroy
  14. # has_many :products, dependent: :destroy
  15. # has_many :invoices, dependent: :destroy
  16. # has_many :expenses, dependent: :destroy
  17. # end
  18. #
  19. # Using <tt>with_options</tt>, we can remove the duplication:
  20. #
  21. # class Account < ActiveRecord::Base
  22. # with_options dependent: :destroy do |assoc|
  23. # assoc.has_many :customers
  24. # assoc.has_many :products
  25. # assoc.has_many :invoices
  26. # assoc.has_many :expenses
  27. # end
  28. # end
  29. #
  30. # It can also be used with an explicit receiver:
  31. #
  32. # I18n.with_options locale: user.locale, scope: 'newsletter' do |i18n|
  33. # subject i18n.t :subject
  34. # body i18n.t :body, user_name: user.name
  35. # end
  36. #
  37. # When you don't pass an explicit receiver, it executes the whole block
  38. # in merging options context:
  39. #
  40. # class Account < ActiveRecord::Base
  41. # with_options dependent: :destroy do
  42. # has_many :customers
  43. # has_many :products
  44. # has_many :invoices
  45. # has_many :expenses
  46. # end
  47. # end
  48. #
  49. # <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
  50. #
  51. # NOTE: Each nesting level will merge inherited defaults in addition to their own.
  52. #
  53. # class Post < ActiveRecord::Base
  54. # with_options if: :persisted?, length: { minimum: 50 } do
  55. # validates :content, if: -> { content.present? }
  56. # end
  57. # end
  58. #
  59. # The code is equivalent to:
  60. #
  61. # validates :content, length: { minimum: 50 }, if: -> { content.present? }
  62. #
  63. # Hence the inherited default for +if+ key is ignored.
  64. #
  65. # NOTE: You cannot call class methods implicitly inside of with_options.
  66. # You can access these methods using the class name instead:
  67. #
  68. # class Phone < ActiveRecord::Base
  69. # enum phone_number_type: { home: 0, office: 1, mobile: 2 }
  70. #
  71. # with_options presence: true do
  72. # validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys }
  73. # end
  74. # end
  75. #
  76. 2 def with_options(options, &block)
  77. option_merger = ActiveSupport::OptionMerger.new(self, options)
  78. block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
  79. end
  80. end

lib/active_support/core_ext/range.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/range/conversions"
  3. 1 require "active_support/core_ext/range/compare_range"
  4. 1 require "active_support/core_ext/range/include_time_with_zone"
  5. 1 require "active_support/core_ext/range/overlaps"
  6. 1 require "active_support/core_ext/range/each"

lib/active_support/core_ext/range/compare_range.rb

22.22% lines covered

27 relevant lines. 6 lines covered and 21 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport
  3. 1 module CompareWithRange
  4. # Extends the default Range#=== to support range comparisons.
  5. # (1..5) === (1..5) # => true
  6. # (1..5) === (2..3) # => true
  7. # (1..5) === (1...6) # => true
  8. # (1..5) === (2..6) # => false
  9. #
  10. # The native Range#=== behavior is untouched.
  11. # ('a'..'f') === ('c') # => true
  12. # (5..9) === (11) # => false
  13. #
  14. # The given range must be fully bounded, with both start and end.
  15. 1 def ===(value)
  16. if value.is_a?(::Range)
  17. is_backwards_op = value.exclude_end? ? :>= : :>
  18. return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
  19. # 1...10 includes 1..9 but it does not include 1..10.
  20. # 1..10 includes 1...11 but it does not include 1...12.
  21. operator = exclude_end? && !value.exclude_end? ? :< : :<=
  22. value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
  23. super(value.first) && (self.end.nil? || value_max.send(operator, last))
  24. else
  25. super
  26. end
  27. end
  28. # Extends the default Range#include? to support range comparisons.
  29. # (1..5).include?(1..5) # => true
  30. # (1..5).include?(2..3) # => true
  31. # (1..5).include?(1...6) # => true
  32. # (1..5).include?(2..6) # => false
  33. #
  34. # The native Range#include? behavior is untouched.
  35. # ('a'..'f').include?('c') # => true
  36. # (5..9).include?(11) # => false
  37. #
  38. # The given range must be fully bounded, with both start and end.
  39. 1 def include?(value)
  40. if value.is_a?(::Range)
  41. is_backwards_op = value.exclude_end? ? :>= : :>
  42. return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
  43. # 1...10 includes 1..9 but it does not include 1..10.
  44. # 1..10 includes 1...11 but it does not include 1...12.
  45. operator = exclude_end? && !value.exclude_end? ? :< : :<=
  46. value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
  47. super(value.first) && (self.end.nil? || value_max.send(operator, last))
  48. else
  49. super
  50. end
  51. end
  52. # Extends the default Range#cover? to support range comparisons.
  53. # (1..5).cover?(1..5) # => true
  54. # (1..5).cover?(2..3) # => true
  55. # (1..5).cover?(1...6) # => true
  56. # (1..5).cover?(2..6) # => false
  57. #
  58. # The native Range#cover? behavior is untouched.
  59. # ('a'..'f').cover?('c') # => true
  60. # (5..9).cover?(11) # => false
  61. #
  62. # The given range must be fully bounded, with both start and end.
  63. 1 def cover?(value)
  64. if value.is_a?(::Range)
  65. is_backwards_op = value.exclude_end? ? :>= : :>
  66. return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
  67. # 1...10 covers 1..9 but it does not cover 1..10.
  68. # 1..10 covers 1...11 but it does not cover 1...12.
  69. operator = exclude_end? && !value.exclude_end? ? :< : :<=
  70. value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
  71. super(value.first) && (self.end.nil? || value_max.send(operator, last))
  72. else
  73. super
  74. end
  75. end
  76. end
  77. end
  78. 1 Range.prepend(ActiveSupport::CompareWithRange)

lib/active_support/core_ext/range/conversions.rb

53.85% lines covered

13 relevant lines. 7 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport
  3. 1 module RangeWithFormat
  4. 1 RANGE_FORMATS = {
  5. db: -> (start, stop) do
  6. case start
  7. when String then "BETWEEN '#{start}' AND '#{stop}'"
  8. else
  9. "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
  10. end
  11. end
  12. }
  13. # Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
  14. #
  15. # range = (1..100) # => 1..100
  16. #
  17. # range.to_s # => "1..100"
  18. # range.to_s(:db) # => "BETWEEN '1' AND '100'"
  19. #
  20. # == Adding your own range formats to to_s
  21. # You can add your own formats to the Range::RANGE_FORMATS hash.
  22. # Use the format name as the hash key and a Proc instance.
  23. #
  24. # # config/initializers/range_formats.rb
  25. # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" }
  26. 1 def to_s(format = :default)
  27. if formatter = RANGE_FORMATS[format]
  28. formatter.call(first, last)
  29. else
  30. super()
  31. end
  32. end
  33. 1 alias_method :to_default_s, :to_s
  34. 1 alias_method :to_formatted_s, :to_s
  35. end
  36. end
  37. 1 Range.prepend(ActiveSupport::RangeWithFormat)

lib/active_support/core_ext/range/each.rb

84.62% lines covered

13 relevant lines. 11 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/time_with_zone"
  3. 1 module ActiveSupport
  4. 1 module EachTimeWithZone #:nodoc:
  5. 1 def each(&block)
  6. 5 ensure_iteration_allowed
  7. 5 super
  8. end
  9. 1 def step(n = 1, &block)
  10. ensure_iteration_allowed
  11. super
  12. end
  13. 1 private
  14. 1 def ensure_iteration_allowed
  15. 5 raise TypeError, "can't iterate from #{first.class}" if first.is_a?(TimeWithZone)
  16. end
  17. end
  18. end
  19. 1 Range.prepend(ActiveSupport::EachTimeWithZone)

lib/active_support/core_ext/range/include_range.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/deprecation"
  3. ActiveSupport::Deprecation.warn "You have required `active_support/core_ext/range/include_range`. " \
  4. "This file will be removed in Rails 6.1. You should require `active_support/core_ext/range/compare_range` " \
  5. "instead."
  6. require "active_support/core_ext/range/compare_range"

lib/active_support/core_ext/range/include_time_with_zone.rb

60.0% lines covered

10 relevant lines. 6 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/time_with_zone"
  3. 1 require "active_support/deprecation"
  4. 1 module ActiveSupport
  5. 1 module IncludeTimeWithZone #:nodoc:
  6. # Extends the default Range#include? to support ActiveSupport::TimeWithZone.
  7. #
  8. # (1.hour.ago..1.hour.from_now).include?(Time.current) # => true
  9. #
  10. 1 def include?(value)
  11. if self.begin.is_a?(TimeWithZone) || self.end.is_a?(TimeWithZone)
  12. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  13. Using `Range#include?` to check the inclusion of a value in
  14. a date time range is deprecated.
  15. It is recommended to use `Range#cover?` instead of `Range#include?` to
  16. check the inclusion of a value in a date time range.
  17. MSG
  18. cover?(value)
  19. else
  20. super
  21. end
  22. end
  23. end
  24. end
  25. 1 Range.prepend(ActiveSupport::IncludeTimeWithZone)

lib/active_support/core_ext/range/overlaps.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Range
  3. # Compare two ranges and see if they overlap each other
  4. # (1..5).overlaps?(4..6) # => true
  5. # (1..5).overlaps?(7..9) # => false
  6. 1 def overlaps?(other)
  7. cover?(other.first) || other.cover?(first)
  8. end
  9. end

lib/active_support/core_ext/regexp.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Regexp #:nodoc:
  3. 1 def multiline?
  4. options & MULTILINE == MULTILINE
  5. end
  6. end

lib/active_support/core_ext/securerandom.rb

42.86% lines covered

14 relevant lines. 6 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "securerandom"
  3. 1 module SecureRandom
  4. 1 BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
  5. 1 BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a
  6. # SecureRandom.base58 generates a random base58 string.
  7. #
  8. # The argument _n_ specifies the length of the random string to be generated.
  9. #
  10. # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
  11. #
  12. # The result may contain alphanumeric characters except 0, O, I and l.
  13. #
  14. # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
  15. # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
  16. 1 def self.base58(n = 16)
  17. SecureRandom.random_bytes(n).unpack("C*").map do |byte|
  18. idx = byte % 64
  19. idx = SecureRandom.random_number(58) if idx >= 58
  20. BASE58_ALPHABET[idx]
  21. end.join
  22. end
  23. # SecureRandom.base36 generates a random base36 string in lowercase.
  24. #
  25. # The argument _n_ specifies the length of the random string to be generated.
  26. #
  27. # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
  28. # This method can be used over +base58+ if a deterministic case key is necessary.
  29. #
  30. # The result will contain alphanumeric characters in lowercase.
  31. #
  32. # p SecureRandom.base36 # => "4kugl2pdqmscqtje"
  33. # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
  34. 1 def self.base36(n = 16)
  35. SecureRandom.random_bytes(n).unpack("C*").map do |byte|
  36. idx = byte % 64
  37. idx = SecureRandom.random_number(36) if idx >= 36
  38. BASE36_ALPHABET[idx]
  39. end.join
  40. end
  41. end

lib/active_support/core_ext/string.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/string/conversions"
  3. 1 require "active_support/core_ext/string/filters"
  4. 1 require "active_support/core_ext/string/multibyte"
  5. 1 require "active_support/core_ext/string/starts_ends_with"
  6. 1 require "active_support/core_ext/string/inflections"
  7. 1 require "active_support/core_ext/string/access"
  8. 1 require "active_support/core_ext/string/behavior"
  9. 1 require "active_support/core_ext/string/output_safety"
  10. 1 require "active_support/core_ext/string/exclude"
  11. 1 require "active_support/core_ext/string/strip"
  12. 1 require "active_support/core_ext/string/inquiry"
  13. 1 require "active_support/core_ext/string/indent"
  14. 1 require "active_support/core_ext/string/zones"

lib/active_support/core_ext/string/access.rb

50.0% lines covered

12 relevant lines. 6 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. # If you pass a single integer, returns a substring of one character at that
  4. # position. The first character of the string is at position 0, the next at
  5. # position 1, and so on. If a range is supplied, a substring containing
  6. # characters at offsets given by the range is returned. In both cases, if an
  7. # offset is negative, it is counted from the end of the string. Returns +nil+
  8. # if the initial offset falls outside the string. Returns an empty string if
  9. # the beginning of the range is greater than the end of the string.
  10. #
  11. # str = "hello"
  12. # str.at(0) # => "h"
  13. # str.at(1..3) # => "ell"
  14. # str.at(-2) # => "l"
  15. # str.at(-2..-1) # => "lo"
  16. # str.at(5) # => nil
  17. # str.at(5..-1) # => ""
  18. #
  19. # If a Regexp is given, the matching portion of the string is returned.
  20. # If a String is given, that given string is returned if it occurs in
  21. # the string. In both cases, +nil+ is returned if there is no match.
  22. #
  23. # str = "hello"
  24. # str.at(/lo/) # => "lo"
  25. # str.at(/ol/) # => nil
  26. # str.at("lo") # => "lo"
  27. # str.at("ol") # => nil
  28. 1 def at(position)
  29. self[position]
  30. end
  31. # Returns a substring from the given position to the end of the string.
  32. # If the position is negative, it is counted from the end of the string.
  33. #
  34. # str = "hello"
  35. # str.from(0) # => "hello"
  36. # str.from(3) # => "lo"
  37. # str.from(-2) # => "lo"
  38. #
  39. # You can mix it with +to+ method and do fun things like:
  40. #
  41. # str = "hello"
  42. # str.from(0).to(-1) # => "hello"
  43. # str.from(1).to(-2) # => "ell"
  44. 1 def from(position)
  45. self[position, length]
  46. end
  47. # Returns a substring from the beginning of the string to the given position.
  48. # If the position is negative, it is counted from the end of the string.
  49. #
  50. # str = "hello"
  51. # str.to(0) # => "h"
  52. # str.to(3) # => "hell"
  53. # str.to(-2) # => "hell"
  54. #
  55. # You can mix it with +from+ method and do fun things like:
  56. #
  57. # str = "hello"
  58. # str.from(0).to(-1) # => "hello"
  59. # str.from(1).to(-2) # => "ell"
  60. 1 def to(position)
  61. position += size if position < 0
  62. self[0, position + 1] || +""
  63. end
  64. # Returns the first character. If a limit is supplied, returns a substring
  65. # from the beginning of the string until it reaches the limit value. If the
  66. # given limit is greater than or equal to the string length, returns a copy of self.
  67. #
  68. # str = "hello"
  69. # str.first # => "h"
  70. # str.first(1) # => "h"
  71. # str.first(2) # => "he"
  72. # str.first(0) # => ""
  73. # str.first(6) # => "hello"
  74. 1 def first(limit = 1)
  75. self[0, limit] || raise(ArgumentError, "negative limit")
  76. end
  77. # Returns the last character of the string. If a limit is supplied, returns a substring
  78. # from the end of the string until it reaches the limit value (counting backwards). If
  79. # the given limit is greater than or equal to the string length, returns a copy of self.
  80. #
  81. # str = "hello"
  82. # str.last # => "o"
  83. # str.last(1) # => "o"
  84. # str.last(2) # => "lo"
  85. # str.last(0) # => ""
  86. # str.last(6) # => "hello"
  87. 1 def last(limit = 1)
  88. self[[length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
  89. end
  90. end

lib/active_support/core_ext/string/behavior.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. # Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
  4. 1 def acts_like_string?
  5. true
  6. end
  7. end

lib/active_support/core_ext/string/conversions.rb

40.0% lines covered

15 relevant lines. 6 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "date"
  3. 2 require "active_support/core_ext/time/calculations"
  4. 2 class String
  5. # Converts a string to a Time value.
  6. # The +form+ can be either :utc or :local (default :local).
  7. #
  8. # The time is parsed using Time.parse method.
  9. # If +form+ is :local, then the time is in the system timezone.
  10. # If the date part is missing then the current date is used and if
  11. # the time part is missing then it is assumed to be 00:00:00.
  12. #
  13. # "13-12-2012".to_time # => 2012-12-13 00:00:00 +0100
  14. # "06:12".to_time # => 2012-12-13 06:12:00 +0100
  15. # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100
  16. # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100
  17. # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC
  18. # "12/13/2012".to_time # => ArgumentError: argument out of range
  19. 2 def to_time(form = :local)
  20. parts = Date._parse(self, false)
  21. used_keys = %i(year mon mday hour min sec sec_fraction offset)
  22. return if (parts.keys & used_keys).empty?
  23. now = Time.now
  24. time = Time.new(
  25. parts.fetch(:year, now.year),
  26. parts.fetch(:mon, now.month),
  27. parts.fetch(:mday, now.day),
  28. parts.fetch(:hour, 0),
  29. parts.fetch(:min, 0),
  30. parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
  31. parts.fetch(:offset, form == :utc ? 0 : nil)
  32. )
  33. form == :utc ? time.utc : time.to_time
  34. end
  35. # Converts a string to a Date value.
  36. #
  37. # "1-1-2012".to_date # => Sun, 01 Jan 2012
  38. # "01/01/2012".to_date # => Sun, 01 Jan 2012
  39. # "2012-12-13".to_date # => Thu, 13 Dec 2012
  40. # "12/13/2012".to_date # => ArgumentError: invalid date
  41. 2 def to_date
  42. ::Date.parse(self, false) unless blank?
  43. end
  44. # Converts a string to a DateTime value.
  45. #
  46. # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
  47. # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
  48. # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
  49. # "12/13/2012".to_datetime # => ArgumentError: invalid date
  50. 2 def to_datetime
  51. ::DateTime.parse(self, false) unless blank?
  52. end
  53. end

lib/active_support/core_ext/string/exclude.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. # The inverse of <tt>String#include?</tt>. Returns true if the string
  4. # does not include the other string.
  5. #
  6. # "hello".exclude? "lo" # => false
  7. # "hello".exclude? "ol" # => true
  8. # "hello".exclude? ?h # => false
  9. 1 def exclude?(string)
  10. !include?(string)
  11. end
  12. end

lib/active_support/core_ext/string/filters.rb

19.51% lines covered

41 relevant lines. 8 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 class String
  3. # Returns the string, first removing all whitespace on both ends of
  4. # the string, and then changing remaining consecutive whitespace
  5. # groups into one space each.
  6. #
  7. # Note that it handles both ASCII and Unicode whitespace.
  8. #
  9. # %{ Multi-line
  10. # string }.squish # => "Multi-line string"
  11. # " foo bar \n \t boo".squish # => "foo bar boo"
  12. 23 def squish
  13. dup.squish!
  14. end
  15. # Performs a destructive squish. See String#squish.
  16. # str = " foo bar \n \t boo"
  17. # str.squish! # => "foo bar boo"
  18. # str # => "foo bar boo"
  19. 23 def squish!
  20. gsub!(/[[:space:]]+/, " ")
  21. strip!
  22. self
  23. end
  24. # Returns a new string with all occurrences of the patterns removed.
  25. # str = "foo bar test"
  26. # str.remove(" test") # => "foo bar"
  27. # str.remove(" test", /bar/) # => "foo "
  28. # str # => "foo bar test"
  29. 23 def remove(*patterns)
  30. dup.remove!(*patterns)
  31. end
  32. # Alters the string by removing all occurrences of the patterns.
  33. # str = "foo bar test"
  34. # str.remove!(" test", /bar/) # => "foo "
  35. # str # => "foo "
  36. 23 def remove!(*patterns)
  37. patterns.each do |pattern|
  38. gsub! pattern, ""
  39. end
  40. self
  41. end
  42. # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
  43. #
  44. # 'Once upon a time in a world far far away'.truncate(27)
  45. # # => "Once upon a time in a wo..."
  46. #
  47. # Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
  48. #
  49. # 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
  50. # # => "Once upon a time in a..."
  51. #
  52. # 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
  53. # # => "Once upon a time in a..."
  54. #
  55. # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
  56. # for a total length not exceeding <tt>length</tt>:
  57. #
  58. # 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
  59. # # => "And they f... (continued)"
  60. 23 def truncate(truncate_at, options = {})
  61. return dup unless length > truncate_at
  62. omission = options[:omission] || "..."
  63. length_with_room_for_omission = truncate_at - omission.length
  64. stop = \
  65. if options[:separator]
  66. rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
  67. else
  68. length_with_room_for_omission
  69. end
  70. +"#{self[0, stop]}#{omission}"
  71. end
  72. # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
  73. # breaking string encoding by splitting multibyte characters or breaking
  74. # grapheme clusters ("perceptual characters") by truncating at combining
  75. # characters.
  76. #
  77. # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
  78. # => 20
  79. # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
  80. # => 80
  81. # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
  82. # => "🔪🔪🔪🔪…"
  83. #
  84. # The truncated text ends with the <tt>:omission</tt> string, defaulting
  85. # to "…", for a total length not exceeding <tt>bytesize</tt>.
  86. 23 def truncate_bytes(truncate_at, omission: "…")
  87. omission ||= ""
  88. case
  89. when bytesize <= truncate_at
  90. dup
  91. when omission.bytesize > truncate_at
  92. raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
  93. when omission.bytesize == truncate_at
  94. omission.dup
  95. else
  96. self.class.new.tap do |cut|
  97. cut_at = truncate_at - omission.bytesize
  98. scan(/\X/) do |grapheme|
  99. if cut.bytesize + grapheme.bytesize <= cut_at
  100. cut << grapheme
  101. else
  102. break
  103. end
  104. end
  105. cut << omission
  106. end
  107. end
  108. end
  109. # Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
  110. #
  111. # 'Once upon a time in a world far far away'.truncate_words(4)
  112. # # => "Once upon a time..."
  113. #
  114. # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
  115. #
  116. # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
  117. # # => "Once<br>upon<br>a<br>time<br>in..."
  118. #
  119. # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
  120. #
  121. # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
  122. # # => "And they found that many... (continued)"
  123. 23 def truncate_words(words_count, options = {})
  124. sep = options[:separator] || /\s+/
  125. sep = Regexp.escape(sep.to_s) unless Regexp === sep
  126. if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
  127. $1 + (options[:omission] || "...")
  128. else
  129. dup
  130. end
  131. end
  132. end

lib/active_support/core_ext/string/indent.rb

42.86% lines covered

7 relevant lines. 3 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. # Same as +indent+, except it indents the receiver in-place.
  4. #
  5. # Returns the indented string, or +nil+ if there was nothing to indent.
  6. 1 def indent!(amount, indent_string = nil, indent_empty_lines = false)
  7. indent_string = indent_string || self[/^[ \t]/] || " "
  8. re = indent_empty_lines ? /^/ : /^(?!$)/
  9. gsub!(re, indent_string * amount)
  10. end
  11. # Indents the lines in the receiver:
  12. #
  13. # <<EOS.indent(2)
  14. # def some_method
  15. # some_code
  16. # end
  17. # EOS
  18. # # =>
  19. # def some_method
  20. # some_code
  21. # end
  22. #
  23. # The second argument, +indent_string+, specifies which indent string to
  24. # use. The default is +nil+, which tells the method to make a guess by
  25. # peeking at the first indented line, and fallback to a space if there is
  26. # none.
  27. #
  28. # " foo".indent(2) # => " foo"
  29. # "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
  30. # "foo".indent(2, "\t") # => "\t\tfoo"
  31. #
  32. # While +indent_string+ is typically one space or tab, it may be any string.
  33. #
  34. # The third argument, +indent_empty_lines+, is a flag that says whether
  35. # empty lines should be indented. Default is false.
  36. #
  37. # "foo\n\nbar".indent(2) # => " foo\n\n bar"
  38. # "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
  39. #
  40. 1 def indent(amount, indent_string = nil, indent_empty_lines = false)
  41. dup.tap { |_| _.indent!(amount, indent_string, indent_empty_lines) }
  42. end
  43. end

lib/active_support/core_ext/string/inflections.rb

55.81% lines covered

43 relevant lines. 24 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/inflector/methods"
  3. 23 require "active_support/inflector/transliterate"
  4. # String inflections define new methods on the String class to transform names for different purposes.
  5. # For instance, you can figure out the name of a table from the name of a class.
  6. #
  7. # 'ScaleScore'.tableize # => "scale_scores"
  8. #
  9. 23 class String
  10. # Returns the plural form of the word in the string.
  11. #
  12. # If the optional parameter +count+ is specified,
  13. # the singular form will be returned if <tt>count == 1</tt>.
  14. # For any other value of +count+ the plural will be returned.
  15. #
  16. # If the optional parameter +locale+ is specified,
  17. # the word will be pluralized as a word of that language.
  18. # By default, this parameter is set to <tt>:en</tt>.
  19. # You must define your own inflection rules for languages other than English.
  20. #
  21. # 'post'.pluralize # => "posts"
  22. # 'octopus'.pluralize # => "octopi"
  23. # 'sheep'.pluralize # => "sheep"
  24. # 'words'.pluralize # => "words"
  25. # 'the blue mailman'.pluralize # => "the blue mailmen"
  26. # 'CamelOctopus'.pluralize # => "CamelOctopi"
  27. # 'apple'.pluralize(1) # => "apple"
  28. # 'apple'.pluralize(2) # => "apples"
  29. # 'ley'.pluralize(:es) # => "leyes"
  30. # 'ley'.pluralize(1, :es) # => "ley"
  31. #
  32. # See ActiveSupport::Inflector.pluralize.
  33. 23 def pluralize(count = nil, locale = :en)
  34. locale = count if count.is_a?(Symbol)
  35. if count == 1
  36. dup
  37. else
  38. ActiveSupport::Inflector.pluralize(self, locale)
  39. end
  40. end
  41. # The reverse of +pluralize+, returns the singular form of a word in a string.
  42. #
  43. # If the optional parameter +locale+ is specified,
  44. # the word will be singularized as a word of that language.
  45. # By default, this parameter is set to <tt>:en</tt>.
  46. # You must define your own inflection rules for languages other than English.
  47. #
  48. # 'posts'.singularize # => "post"
  49. # 'octopi'.singularize # => "octopus"
  50. # 'sheep'.singularize # => "sheep"
  51. # 'word'.singularize # => "word"
  52. # 'the blue mailmen'.singularize # => "the blue mailman"
  53. # 'CamelOctopi'.singularize # => "CamelOctopus"
  54. # 'leyes'.singularize(:es) # => "ley"
  55. #
  56. # See ActiveSupport::Inflector.singularize.
  57. 23 def singularize(locale = :en)
  58. ActiveSupport::Inflector.singularize(self, locale)
  59. end
  60. # +constantize+ tries to find a declared constant with the name specified
  61. # in the string. It raises a NameError when the name is not in CamelCase
  62. # or is not initialized.
  63. #
  64. # 'Module'.constantize # => Module
  65. # 'Class'.constantize # => Class
  66. # 'blargle'.constantize # => NameError: wrong constant name blargle
  67. #
  68. # See ActiveSupport::Inflector.constantize.
  69. 23 def constantize
  70. ActiveSupport::Inflector.constantize(self)
  71. end
  72. # +safe_constantize+ tries to find a declared constant with the name specified
  73. # in the string. It returns +nil+ when the name is not in CamelCase
  74. # or is not initialized.
  75. #
  76. # 'Module'.safe_constantize # => Module
  77. # 'Class'.safe_constantize # => Class
  78. # 'blargle'.safe_constantize # => nil
  79. #
  80. # See ActiveSupport::Inflector.safe_constantize.
  81. 23 def safe_constantize
  82. ActiveSupport::Inflector.safe_constantize(self)
  83. end
  84. # By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize
  85. # is set to <tt>:lower</tt> then camelize produces lowerCamelCase.
  86. #
  87. # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
  88. #
  89. # 'active_record'.camelize # => "ActiveRecord"
  90. # 'active_record'.camelize(:lower) # => "activeRecord"
  91. # 'active_record/errors'.camelize # => "ActiveRecord::Errors"
  92. # 'active_record/errors'.camelize(:lower) # => "activeRecord::Errors"
  93. #
  94. # +camelize+ is also aliased as +camelcase+.
  95. #
  96. # See ActiveSupport::Inflector.camelize.
  97. 23 def camelize(first_letter = :upper)
  98. 3 case first_letter
  99. when :upper
  100. 3 ActiveSupport::Inflector.camelize(self, true)
  101. when :lower
  102. ActiveSupport::Inflector.camelize(self, false)
  103. else
  104. raise ArgumentError, "Invalid option, use either :upper or :lower."
  105. end
  106. end
  107. 23 alias_method :camelcase, :camelize
  108. # Capitalizes all the words and replaces some characters in the string to create
  109. # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
  110. # used in the Rails internals.
  111. #
  112. # The trailing '_id','Id'.. can be kept and capitalized by setting the
  113. # optional parameter +keep_id_suffix+ to true.
  114. # By default, this parameter is false.
  115. #
  116. # 'man from the boondocks'.titleize # => "Man From The Boondocks"
  117. # 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
  118. # 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id"
  119. #
  120. # +titleize+ is also aliased as +titlecase+.
  121. #
  122. # See ActiveSupport::Inflector.titleize.
  123. 23 def titleize(keep_id_suffix: false)
  124. ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix)
  125. end
  126. 23 alias_method :titlecase, :titleize
  127. # The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string.
  128. #
  129. # +underscore+ will also change '::' to '/' to convert namespaces to paths.
  130. #
  131. # 'ActiveModel'.underscore # => "active_model"
  132. # 'ActiveModel::Errors'.underscore # => "active_model/errors"
  133. #
  134. # See ActiveSupport::Inflector.underscore.
  135. 23 def underscore
  136. 730 ActiveSupport::Inflector.underscore(self)
  137. end
  138. # Replaces underscores with dashes in the string.
  139. #
  140. # 'puni_puni'.dasherize # => "puni-puni"
  141. #
  142. # See ActiveSupport::Inflector.dasherize.
  143. 23 def dasherize
  144. ActiveSupport::Inflector.dasherize(self)
  145. end
  146. # Removes the module part from the constant expression in the string.
  147. #
  148. # 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections"
  149. # 'Inflections'.demodulize # => "Inflections"
  150. # '::Inflections'.demodulize # => "Inflections"
  151. # ''.demodulize # => ''
  152. #
  153. # See ActiveSupport::Inflector.demodulize.
  154. #
  155. # See also +deconstantize+.
  156. 23 def demodulize
  157. ActiveSupport::Inflector.demodulize(self)
  158. end
  159. # Removes the rightmost segment from the constant expression in the string.
  160. #
  161. # 'Net::HTTP'.deconstantize # => "Net"
  162. # '::Net::HTTP'.deconstantize # => "::Net"
  163. # 'String'.deconstantize # => ""
  164. # '::String'.deconstantize # => ""
  165. # ''.deconstantize # => ""
  166. #
  167. # See ActiveSupport::Inflector.deconstantize.
  168. #
  169. # See also +demodulize+.
  170. 23 def deconstantize
  171. ActiveSupport::Inflector.deconstantize(self)
  172. end
  173. # Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
  174. #
  175. # If the optional parameter +locale+ is specified,
  176. # the word will be parameterized as a word of that language.
  177. # By default, this parameter is set to <tt>nil</tt> and it will use
  178. # the configured <tt>I18n.locale</tt>.
  179. #
  180. # class Person
  181. # def to_param
  182. # "#{id}-#{name.parameterize}"
  183. # end
  184. # end
  185. #
  186. # @person = Person.find(1)
  187. # # => #<Person id: 1, name: "Donald E. Knuth">
  188. #
  189. # <%= link_to(@person.name, person_path) %>
  190. # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
  191. #
  192. # To preserve the case of the characters in a string, use the +preserve_case+ argument.
  193. #
  194. # class Person
  195. # def to_param
  196. # "#{id}-#{name.parameterize(preserve_case: true)}"
  197. # end
  198. # end
  199. #
  200. # @person = Person.find(1)
  201. # # => #<Person id: 1, name: "Donald E. Knuth">
  202. #
  203. # <%= link_to(@person.name, person_path) %>
  204. # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a>
  205. #
  206. # See ActiveSupport::Inflector.parameterize.
  207. 23 def parameterize(separator: "-", preserve_case: false, locale: nil)
  208. ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case, locale: locale)
  209. end
  210. # Creates the name of a table like Rails does for models to table names. This method
  211. # uses the +pluralize+ method on the last word in the string.
  212. #
  213. # 'RawScaledScorer'.tableize # => "raw_scaled_scorers"
  214. # 'ham_and_egg'.tableize # => "ham_and_eggs"
  215. # 'fancyCategory'.tableize # => "fancy_categories"
  216. #
  217. # See ActiveSupport::Inflector.tableize.
  218. 23 def tableize
  219. ActiveSupport::Inflector.tableize(self)
  220. end
  221. # Creates a class name from a plural table name like Rails does for table names to models.
  222. # Note that this returns a string and not a class. (To convert to an actual class
  223. # follow +classify+ with +constantize+.)
  224. #
  225. # 'ham_and_eggs'.classify # => "HamAndEgg"
  226. # 'posts'.classify # => "Post"
  227. #
  228. # See ActiveSupport::Inflector.classify.
  229. 23 def classify
  230. ActiveSupport::Inflector.classify(self)
  231. end
  232. # Capitalizes the first word, turns underscores into spaces, and (by default)strips a
  233. # trailing '_id' if present.
  234. # Like +titleize+, this is meant for creating pretty output.
  235. #
  236. # The capitalization of the first word can be turned off by setting the
  237. # optional parameter +capitalize+ to false.
  238. # By default, this parameter is true.
  239. #
  240. # The trailing '_id' can be kept and capitalized by setting the
  241. # optional parameter +keep_id_suffix+ to true.
  242. # By default, this parameter is false.
  243. #
  244. # 'employee_salary'.humanize # => "Employee salary"
  245. # 'author_id'.humanize # => "Author"
  246. # 'author_id'.humanize(capitalize: false) # => "author"
  247. # '_id'.humanize # => "Id"
  248. # 'author_id'.humanize(keep_id_suffix: true) # => "Author Id"
  249. #
  250. # See ActiveSupport::Inflector.humanize.
  251. 23 def humanize(capitalize: true, keep_id_suffix: false)
  252. ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix)
  253. end
  254. # Converts just the first character to uppercase.
  255. #
  256. # 'what a Lovely Day'.upcase_first # => "What a Lovely Day"
  257. # 'w'.upcase_first # => "W"
  258. # ''.upcase_first # => ""
  259. #
  260. # See ActiveSupport::Inflector.upcase_first.
  261. 23 def upcase_first
  262. ActiveSupport::Inflector.upcase_first(self)
  263. end
  264. # Creates a foreign key name from a class name.
  265. # +separate_class_name_and_id_with_underscore+ sets whether
  266. # the method should put '_' between the name and 'id'.
  267. #
  268. # 'Message'.foreign_key # => "message_id"
  269. # 'Message'.foreign_key(false) # => "messageid"
  270. # 'Admin::Post'.foreign_key # => "post_id"
  271. #
  272. # See ActiveSupport::Inflector.foreign_key.
  273. 23 def foreign_key(separate_class_name_and_id_with_underscore = true)
  274. ActiveSupport::Inflector.foreign_key(self, separate_class_name_and_id_with_underscore)
  275. end
  276. end

lib/active_support/core_ext/string/inquiry.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/string_inquirer"
  3. 1 require "active_support/environment_inquirer"
  4. 1 class String
  5. # Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class,
  6. # which gives you a prettier way to test for equality.
  7. #
  8. # env = 'production'.inquiry
  9. # env.production? # => true
  10. # env.development? # => false
  11. 1 def inquiry
  12. ActiveSupport::StringInquirer.new(self)
  13. end
  14. end

lib/active_support/core_ext/string/multibyte.rb

44.44% lines covered

9 relevant lines. 4 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/multibyte"
  3. 23 class String
  4. # == Multibyte proxy
  5. #
  6. # +mb_chars+ is a multibyte safe proxy for string methods.
  7. #
  8. # It creates and returns an instance of the ActiveSupport::Multibyte::Chars class which
  9. # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
  10. # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
  11. #
  12. # >> "lj".mb_chars.upcase.to_s
  13. # => "LJ"
  14. #
  15. # NOTE: Ruby 2.4 and later support native Unicode case mappings:
  16. #
  17. # >> "lj".upcase
  18. # => "LJ"
  19. #
  20. # == Method chaining
  21. #
  22. # All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
  23. # method chaining on the result of any of these methods.
  24. #
  25. # name.mb_chars.reverse.length # => 12
  26. #
  27. # == Interoperability and configuration
  28. #
  29. # The Chars object tries to be as interchangeable with String objects as possible: sorting and comparing between
  30. # String and Char work like expected. The bang! methods change the internal string representation in the Chars
  31. # object. Interoperability problems can be resolved easily with a +to_s+ call.
  32. #
  33. # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For
  34. # information about how to change the default Multibyte behavior see ActiveSupport::Multibyte.
  35. 23 def mb_chars
  36. ActiveSupport::Multibyte.proxy_class.new(self)
  37. end
  38. # Returns +true+ if string has utf_8 encoding.
  39. #
  40. # utf_8_str = "some string".encode "UTF-8"
  41. # iso_str = "some string".encode "ISO-8859-1"
  42. #
  43. # utf_8_str.is_utf8? # => true
  44. # iso_str.is_utf8? # => false
  45. 23 def is_utf8?
  46. case encoding
  47. when Encoding::UTF_8, Encoding::US_ASCII
  48. valid_encoding?
  49. when Encoding::ASCII_8BIT
  50. dup.force_encoding(Encoding::UTF_8).valid_encoding?
  51. else
  52. false
  53. end
  54. end
  55. end

lib/active_support/core_ext/string/output_safety.rb

58.72% lines covered

109 relevant lines. 64 lines covered and 45 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "erb"
  3. 1 require "active_support/core_ext/module/redefine_method"
  4. 1 require "active_support/multibyte/unicode"
  5. 1 class ERB
  6. 1 module Util
  7. 1 HTML_ESCAPE = { "&" => "&amp;", ">" => "&gt;", "<" => "&lt;", '"' => "&quot;", "'" => "&#39;" }
  8. 1 JSON_ESCAPE = { "&" => '\u0026', ">" => '\u003e', "<" => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
  9. 1 HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
  10. 1 JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
  11. # A utility method for escaping HTML tag characters.
  12. # This method is also aliased as <tt>h</tt>.
  13. #
  14. # puts html_escape('is a > 0 & a < 10?')
  15. # # => is a &gt; 0 &amp; a &lt; 10?
  16. 1 def html_escape(s)
  17. unwrapped_html_escape(s).html_safe
  18. end
  19. 1 silence_redefinition_of_method :h
  20. 1 alias h html_escape
  21. 1 module_function :h
  22. 1 singleton_class.silence_redefinition_of_method :html_escape
  23. 1 module_function :html_escape
  24. # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
  25. # This method is not for public consumption! Seriously!
  26. 1 def unwrapped_html_escape(s) # :nodoc:
  27. s = s.to_s
  28. if s.html_safe?
  29. s
  30. else
  31. CGI.escapeHTML(ActiveSupport::Multibyte::Unicode.tidy_bytes(s))
  32. end
  33. end
  34. 1 module_function :unwrapped_html_escape
  35. # A utility method for escaping HTML without affecting existing escaped entities.
  36. #
  37. # html_escape_once('1 < 2 &amp; 3')
  38. # # => "1 &lt; 2 &amp; 3"
  39. #
  40. # html_escape_once('&lt;&lt; Accept & Checkout')
  41. # # => "&lt;&lt; Accept &amp; Checkout"
  42. 1 def html_escape_once(s)
  43. result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
  44. s.html_safe? ? result.html_safe : result
  45. end
  46. 1 module_function :html_escape_once
  47. # A utility method for escaping HTML entities in JSON strings. Specifically, the
  48. # &, > and < characters are replaced with their equivalent unicode escaped form -
  49. # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
  50. # escaped as they are treated as newline characters in some JavaScript engines.
  51. # These sequences have identical meaning as the original characters inside the
  52. # context of a JSON string, so assuming the input is a valid and well-formed
  53. # JSON value, the output will have equivalent meaning when parsed:
  54. #
  55. # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
  56. # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
  57. #
  58. # json_escape(json)
  59. # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
  60. #
  61. # JSON.parse(json) == JSON.parse(json_escape(json))
  62. # # => true
  63. #
  64. # The intended use case for this method is to escape JSON strings before including
  65. # them inside a script tag to avoid XSS vulnerability:
  66. #
  67. # <script>
  68. # var currentUser = <%= raw json_escape(current_user.to_json) %>;
  69. # </script>
  70. #
  71. # It is necessary to +raw+ the result of +json_escape+, so that quotation marks
  72. # don't get converted to <tt>&quot;</tt> entities. +json_escape+ doesn't
  73. # automatically flag the result as HTML safe, since the raw value is unsafe to
  74. # use inside HTML attributes.
  75. #
  76. # If your JSON is being used downstream for insertion into the DOM, be aware of
  77. # whether or not it is being inserted via +html()+. Most jQuery plugins do this.
  78. # If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
  79. # content returned by your JSON.
  80. #
  81. # If you need to output JSON elsewhere in your HTML, you can just do something
  82. # like this, as any unsafe characters (including quotation marks) will be
  83. # automatically escaped for you:
  84. #
  85. # <div data-user-info="<%= current_user.to_json %>">...</div>
  86. #
  87. # WARNING: this helper only works with valid JSON. Using this on non-JSON values
  88. # will open up serious XSS vulnerabilities. For example, if you replace the
  89. # +current_user.to_json+ in the example above with user input instead, the browser
  90. # will happily eval() that string as JavaScript.
  91. #
  92. # The escaping performed in this method is identical to those performed in the
  93. # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
  94. # set to true. Because this transformation is idempotent, this helper can be
  95. # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
  96. #
  97. # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
  98. # is enabled, or if you are unsure where your JSON string originated from, it
  99. # is recommended that you always apply this helper (other libraries, such as the
  100. # JSON gem, do not provide this kind of protection by default; also some gems
  101. # might override +to_json+ to bypass Active Support's encoder).
  102. 1 def json_escape(s)
  103. result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
  104. s.html_safe? ? result.html_safe : result
  105. end
  106. 1 module_function :json_escape
  107. end
  108. end
  109. 1 class Object
  110. 1 def html_safe?
  111. false
  112. end
  113. end
  114. 1 class Numeric
  115. 1 def html_safe?
  116. true
  117. end
  118. end
  119. 1 module ActiveSupport #:nodoc:
  120. 1 class SafeBuffer < String
  121. 1 UNSAFE_STRING_METHODS = %w(
  122. capitalize chomp chop delete delete_prefix delete_suffix
  123. downcase lstrip next reverse rstrip slice squeeze strip
  124. succ swapcase tr tr_s unicode_normalize upcase
  125. )
  126. 1 UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub)
  127. 1 alias_method :original_concat, :concat
  128. 1 private :original_concat
  129. # Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers.
  130. 1 class SafeConcatError < StandardError
  131. 1 def initialize
  132. super "Could not concatenate to the buffer because it is not html safe."
  133. end
  134. end
  135. 1 def [](*args)
  136. if html_safe?
  137. new_safe_buffer = super
  138. if new_safe_buffer
  139. new_safe_buffer.instance_variable_set :@html_safe, true
  140. end
  141. new_safe_buffer
  142. else
  143. to_str[*args]
  144. end
  145. end
  146. 1 def safe_concat(value)
  147. raise SafeConcatError unless html_safe?
  148. original_concat(value)
  149. end
  150. 1 def initialize(str = "")
  151. 1 @html_safe = true
  152. 1 super
  153. end
  154. 1 def initialize_copy(other)
  155. super
  156. @html_safe = other.html_safe?
  157. end
  158. 1 def clone_empty
  159. self[0, 0]
  160. end
  161. 1 def concat(value)
  162. super(html_escape_interpolated_argument(value))
  163. end
  164. 1 alias << concat
  165. 1 def insert(index, value)
  166. super(index, html_escape_interpolated_argument(value))
  167. end
  168. 1 def prepend(value)
  169. super(html_escape_interpolated_argument(value))
  170. end
  171. 1 def replace(value)
  172. super(html_escape_interpolated_argument(value))
  173. end
  174. 1 def []=(*args)
  175. if args.length == 3
  176. super(args[0], args[1], html_escape_interpolated_argument(args[2]))
  177. else
  178. super(args[0], html_escape_interpolated_argument(args[1]))
  179. end
  180. end
  181. 1 def +(other)
  182. dup.concat(other)
  183. end
  184. 1 def *(*)
  185. new_safe_buffer = super
  186. new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
  187. new_safe_buffer
  188. end
  189. 1 def %(args)
  190. case args
  191. when Hash
  192. escaped_args = args.transform_values { |arg| html_escape_interpolated_argument(arg) }
  193. else
  194. escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
  195. end
  196. self.class.new(super(escaped_args))
  197. end
  198. 1 def html_safe?
  199. defined?(@html_safe) && @html_safe
  200. end
  201. 1 def to_s
  202. self
  203. end
  204. 1 def to_param
  205. to_str
  206. end
  207. 1 def encode_with(coder)
  208. coder.represent_object nil, to_str
  209. end
  210. 1 UNSAFE_STRING_METHODS.each do |unsafe_method|
  211. 20 if unsafe_method.respond_to?(unsafe_method)
  212. 20 class_eval <<-EOT, __FILE__, __LINE__ + 1
  213. def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
  214. to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
  215. end # end
  216. def #{unsafe_method}!(*args) # def capitalize!(*args)
  217. @html_safe = false # @html_safe = false
  218. super # super
  219. end # end
  220. EOT
  221. end
  222. end
  223. 1 UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
  224. 2 if unsafe_method.respond_to?(unsafe_method)
  225. 2 class_eval <<-EOT, __FILE__, __LINE__ + 1
  226. def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
  227. if block # if block
  228. to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
  229. set_block_back_references(block, $~) # set_block_back_references(block, $~)
  230. block.call(*params) # block.call(*params)
  231. } # }
  232. else # else
  233. to_str.#{unsafe_method}(*args) # to_str.gsub(*args)
  234. end # end
  235. end # end
  236. def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
  237. @html_safe = false # @html_safe = false
  238. if block # if block
  239. super(*args) { |*params| # super(*args) { |*params|
  240. set_block_back_references(block, $~) # set_block_back_references(block, $~)
  241. block.call(*params) # block.call(*params)
  242. } # }
  243. else # else
  244. super # super
  245. end # end
  246. end # end
  247. EOT
  248. end
  249. end
  250. 1 private
  251. 1 def html_escape_interpolated_argument(arg)
  252. (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
  253. end
  254. 1 def set_block_back_references(block, match_data)
  255. block.binding.eval("proc { |m| $~ = m }").call(match_data)
  256. rescue ArgumentError
  257. # Can't create binding from C level Proc
  258. end
  259. end
  260. end
  261. 1 class String
  262. # Marks a string as trusted safe. It will be inserted into HTML with no
  263. # additional escaping performed. It is your responsibility to ensure that the
  264. # string contains no malicious content. This method is equivalent to the
  265. # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
  266. # this method. It should never be called on user input.
  267. 1 def html_safe
  268. ActiveSupport::SafeBuffer.new(self)
  269. end
  270. end

lib/active_support/core_ext/string/starts_ends_with.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. 1 alias :starts_with? :start_with?
  4. 1 alias :ends_with? :end_with?
  5. end

lib/active_support/core_ext/string/strip.rb

50.0% lines covered

4 relevant lines. 2 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class String
  3. # Strips indentation in heredocs.
  4. #
  5. # For example in
  6. #
  7. # if options[:usage]
  8. # puts <<-USAGE.strip_heredoc
  9. # This command does such and such.
  10. #
  11. # Supported options are:
  12. # -h This message
  13. # ...
  14. # USAGE
  15. # end
  16. #
  17. # the user would see the usage message aligned against the left margin.
  18. #
  19. # Technically, it looks for the least indented non-empty line
  20. # in the whole string, and removes that amount of leading whitespace.
  21. 1 def strip_heredoc
  22. gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").tap do |stripped|
  23. stripped.freeze if frozen?
  24. end
  25. end
  26. end

lib/active_support/core_ext/string/zones.rb

57.14% lines covered

7 relevant lines. 4 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/string/conversions"
  3. 2 require "active_support/core_ext/time/zones"
  4. 2 class String
  5. # Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
  6. # is set, otherwise converts String to a Time via String#to_time
  7. 2 def in_time_zone(zone = ::Time.zone)
  8. if zone
  9. ::Time.find_zone!(zone).parse(self)
  10. else
  11. to_time
  12. end
  13. end
  14. end

lib/active_support/core_ext/symbol.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/symbol/starts_ends_with"

lib/active_support/core_ext/symbol/starts_ends_with.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Symbol
  3. def start_with?(*prefixes)
  4. 152 to_s.start_with?(*prefixes)
  5. 2 end unless method_defined?(:start_with?)
  6. def end_with?(*suffixes)
  7. to_s.end_with?(*suffixes)
  8. 2 end unless method_defined?(:end_with?)
  9. 2 alias :starts_with? :start_with?
  10. 2 alias :ends_with? :end_with?
  11. end

lib/active_support/core_ext/time.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/time/acts_like"
  3. 2 require "active_support/core_ext/time/calculations"
  4. 2 require "active_support/core_ext/time/compatibility"
  5. 2 require "active_support/core_ext/time/conversions"
  6. 2 require "active_support/core_ext/time/zones"

lib/active_support/core_ext/time/acts_like.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/object/acts_like"
  3. 23 class Time
  4. # Duck-types as a Time-like class. See Object#acts_like?.
  5. 23 def acts_like_time?
  6. true
  7. end
  8. end

lib/active_support/core_ext/time/calculations.rb

46.26% lines covered

147 relevant lines. 68 lines covered and 79 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/duration"
  3. 23 require "active_support/core_ext/time/conversions"
  4. 23 require "active_support/time_with_zone"
  5. 23 require "active_support/core_ext/time/zones"
  6. 23 require "active_support/core_ext/date_and_time/calculations"
  7. 23 require "active_support/core_ext/date/calculations"
  8. 23 class Time
  9. 23 include DateAndTime::Calculations
  10. 23 COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  11. 23 class << self
  12. # Overriding case equality method so that it returns true for ActiveSupport::TimeWithZone instances
  13. 23 def ===(other)
  14. 1996 super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone))
  15. end
  16. # Returns the number of days in the given month.
  17. # If no year is specified, it will use the current year.
  18. 23 def days_in_month(month, year = current.year)
  19. if month == 2 && ::Date.gregorian_leap?(year)
  20. 29
  21. else
  22. COMMON_YEAR_DAYS_IN_MONTH[month]
  23. end
  24. end
  25. # Returns the number of days in the given year.
  26. # If no year is specified, it will use the current year.
  27. 23 def days_in_year(year = current.year)
  28. days_in_month(2, year) + 337
  29. end
  30. # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
  31. 23 def current
  32. ::Time.zone ? ::Time.zone.now : ::Time.now
  33. end
  34. # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
  35. # instances can be used when called with a single argument
  36. 23 def at_with_coercion(*args)
  37. return at_without_coercion(*args) if args.size != 1
  38. # Time.at can be called with a time or numerical value
  39. time_or_number = args.first
  40. if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
  41. at_without_coercion(time_or_number.to_f).getlocal
  42. else
  43. at_without_coercion(time_or_number)
  44. end
  45. end
  46. 23 alias_method :at_without_coercion, :at
  47. 23 alias_method :at, :at_with_coercion
  48. # Creates a +Time+ instance from an RFC 3339 string.
  49. #
  50. # Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000
  51. #
  52. # If the time or offset components are missing then an +ArgumentError+ will be raised.
  53. #
  54. # Time.rfc3339('1999-12-31') # => ArgumentError: invalid date
  55. 23 def rfc3339(str)
  56. parts = Date._rfc3339(str)
  57. raise ArgumentError, "invalid date" if parts.empty?
  58. Time.new(
  59. parts.fetch(:year),
  60. parts.fetch(:mon),
  61. parts.fetch(:mday),
  62. parts.fetch(:hour),
  63. parts.fetch(:min),
  64. parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
  65. parts.fetch(:offset)
  66. )
  67. end
  68. end
  69. # Returns the number of seconds since 00:00:00.
  70. #
  71. # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0.0
  72. # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296.0
  73. # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399.0
  74. 23 def seconds_since_midnight
  75. to_i - change(hour: 0).to_i + (usec / 1.0e+6)
  76. end
  77. # Returns the number of seconds until 23:59:59.
  78. #
  79. # Time.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
  80. # Time.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
  81. # Time.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
  82. 23 def seconds_until_end_of_day
  83. end_of_day.to_i - to_i
  84. end
  85. # Returns the fraction of a second as a +Rational+
  86. #
  87. # Time.new(2012, 8, 29, 0, 0, 0.5).sec_fraction # => (1/2)
  88. 23 def sec_fraction
  89. subsec
  90. end
  91. # Returns a new Time where one or more of the elements have been changed according
  92. # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>,
  93. # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only
  94. # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
  95. # and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter
  96. # takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>,
  97. # <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>,
  98. # <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
  99. #
  100. # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
  101. # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
  102. # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0)
  103. 23 def change(options)
  104. new_year = options.fetch(:year, year)
  105. new_month = options.fetch(:month, month)
  106. new_day = options.fetch(:day, day)
  107. new_hour = options.fetch(:hour, hour)
  108. new_min = options.fetch(:min, options[:hour] ? 0 : min)
  109. new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
  110. new_offset = options.fetch(:offset, nil)
  111. if new_nsec = options[:nsec]
  112. raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
  113. new_usec = Rational(new_nsec, 1000)
  114. else
  115. new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
  116. end
  117. raise ArgumentError, "argument out of range" if new_usec >= 1000000
  118. new_sec += Rational(new_usec, 1000000)
  119. if new_offset
  120. ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset)
  121. elsif utc?
  122. ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec)
  123. elsif zone
  124. ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec)
  125. else
  126. ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset)
  127. end
  128. end
  129. # Uses Date to provide precise Time calculations for years, months, and days
  130. # according to the proleptic Gregorian calendar. The +options+ parameter
  131. # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
  132. # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
  133. # <tt>:seconds</tt>.
  134. #
  135. # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700
  136. # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700
  137. # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700
  138. # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700
  139. # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700
  140. 23 def advance(options)
  141. unless options[:weeks].nil?
  142. options[:weeks], partial_weeks = options[:weeks].divmod(1)
  143. options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
  144. end
  145. unless options[:days].nil?
  146. options[:days], partial_days = options[:days].divmod(1)
  147. options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
  148. end
  149. d = to_date.gregorian.advance(options)
  150. time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
  151. seconds_to_advance = \
  152. options.fetch(:seconds, 0) +
  153. options.fetch(:minutes, 0) * 60 +
  154. options.fetch(:hours, 0) * 3600
  155. if seconds_to_advance.zero?
  156. time_advanced_by_date
  157. else
  158. time_advanced_by_date.since(seconds_to_advance)
  159. end
  160. end
  161. # Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension
  162. 23 def ago(seconds)
  163. since(-seconds)
  164. end
  165. # Returns a new Time representing the time a number of seconds since the instance time
  166. 23 def since(seconds)
  167. self + seconds
  168. rescue
  169. to_datetime.since(seconds)
  170. end
  171. 23 alias :in :since
  172. # Returns a new Time representing the start of the day (0:00)
  173. 23 def beginning_of_day
  174. change(hour: 0)
  175. end
  176. 23 alias :midnight :beginning_of_day
  177. 23 alias :at_midnight :beginning_of_day
  178. 23 alias :at_beginning_of_day :beginning_of_day
  179. # Returns a new Time representing the middle of the day (12:00)
  180. 23 def middle_of_day
  181. change(hour: 12)
  182. end
  183. 23 alias :midday :middle_of_day
  184. 23 alias :noon :middle_of_day
  185. 23 alias :at_midday :middle_of_day
  186. 23 alias :at_noon :middle_of_day
  187. 23 alias :at_middle_of_day :middle_of_day
  188. # Returns a new Time representing the end of the day, 23:59:59.999999
  189. 23 def end_of_day
  190. change(
  191. hour: 23,
  192. min: 59,
  193. sec: 59,
  194. usec: Rational(999999999, 1000)
  195. )
  196. end
  197. 23 alias :at_end_of_day :end_of_day
  198. # Returns a new Time representing the start of the hour (x:00)
  199. 23 def beginning_of_hour
  200. change(min: 0)
  201. end
  202. 23 alias :at_beginning_of_hour :beginning_of_hour
  203. # Returns a new Time representing the end of the hour, x:59:59.999999
  204. 23 def end_of_hour
  205. change(
  206. min: 59,
  207. sec: 59,
  208. usec: Rational(999999999, 1000)
  209. )
  210. end
  211. 23 alias :at_end_of_hour :end_of_hour
  212. # Returns a new Time representing the start of the minute (x:xx:00)
  213. 23 def beginning_of_minute
  214. change(sec: 0)
  215. end
  216. 23 alias :at_beginning_of_minute :beginning_of_minute
  217. # Returns a new Time representing the end of the minute, x:xx:59.999999
  218. 23 def end_of_minute
  219. change(
  220. sec: 59,
  221. usec: Rational(999999999, 1000)
  222. )
  223. end
  224. 23 alias :at_end_of_minute :end_of_minute
  225. 23 def plus_with_duration(other) #:nodoc:
  226. if ActiveSupport::Duration === other
  227. other.since(self)
  228. else
  229. plus_without_duration(other)
  230. end
  231. end
  232. 23 alias_method :plus_without_duration, :+
  233. 23 alias_method :+, :plus_with_duration
  234. 23 def minus_with_duration(other) #:nodoc:
  235. if ActiveSupport::Duration === other
  236. other.until(self)
  237. else
  238. minus_without_duration(other)
  239. end
  240. end
  241. 23 alias_method :minus_without_duration, :-
  242. 23 alias_method :-, :minus_with_duration
  243. # Time#- can also be used to determine the number of seconds between two Time instances.
  244. # We're layering on additional behavior so that ActiveSupport::TimeWithZone instances
  245. # are coerced into values that Time#- will recognize
  246. 23 def minus_with_coercion(other)
  247. other = other.comparable_time if other.respond_to?(:comparable_time)
  248. other.is_a?(DateTime) ? to_f - other.to_f : minus_without_coercion(other)
  249. end
  250. 23 alias_method :minus_without_coercion, :-
  251. 23 alias_method :-, :minus_with_coercion
  252. # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
  253. # can be chronologically compared with a Time
  254. 23 def compare_with_coercion(other)
  255. # we're avoiding Time#to_datetime and Time#to_time because they're expensive
  256. if other.class == Time
  257. compare_without_coercion(other)
  258. elsif other.is_a?(Time)
  259. compare_without_coercion(other.to_time)
  260. else
  261. to_datetime <=> other
  262. end
  263. end
  264. 23 alias_method :compare_without_coercion, :<=>
  265. 23 alias_method :<=>, :compare_with_coercion
  266. # Layers additional behavior on Time#eql? so that ActiveSupport::TimeWithZone instances
  267. # can be eql? to an equivalent Time
  268. 23 def eql_with_coercion(other)
  269. # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do eql? comparison
  270. other = other.comparable_time if other.respond_to?(:comparable_time)
  271. eql_without_coercion(other)
  272. end
  273. 23 alias_method :eql_without_coercion, :eql?
  274. 23 alias_method :eql?, :eql_with_coercion
  275. # Returns a new time the specified number of days ago.
  276. 23 def prev_day(days = 1)
  277. advance(days: -days)
  278. end
  279. # Returns a new time the specified number of days in the future.
  280. 23 def next_day(days = 1)
  281. advance(days: days)
  282. end
  283. # Returns a new time the specified number of months ago.
  284. 23 def prev_month(months = 1)
  285. advance(months: -months)
  286. end
  287. # Returns a new time the specified number of months in the future.
  288. 23 def next_month(months = 1)
  289. advance(months: months)
  290. end
  291. # Returns a new time the specified number of years ago.
  292. 23 def prev_year(years = 1)
  293. advance(years: -years)
  294. end
  295. # Returns a new time the specified number of years in the future.
  296. 23 def next_year(years = 1)
  297. advance(years: years)
  298. end
  299. end

lib/active_support/core_ext/time/compatibility.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/date_and_time/compatibility"
  3. 2 require "active_support/core_ext/module/redefine_method"
  4. 2 class Time
  5. 2 include DateAndTime::Compatibility
  6. 2 silence_redefinition_of_method :to_time
  7. # Either return +self+ or the time in the local system timezone depending
  8. # on the setting of +ActiveSupport.to_time_preserves_timezone+.
  9. 2 def to_time
  10. preserve_timezone ? self : getlocal
  11. end
  12. end

lib/active_support/core_ext/time/conversions.rb

50.0% lines covered

18 relevant lines. 9 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/inflector/methods"
  3. 23 require "active_support/values/time_zone"
  4. 23 class Time
  5. 23 DATE_FORMATS = {
  6. db: "%Y-%m-%d %H:%M:%S",
  7. inspect: "%Y-%m-%d %H:%M:%S.%9N %z",
  8. number: "%Y%m%d%H%M%S",
  9. nsec: "%Y%m%d%H%M%S%9N",
  10. usec: "%Y%m%d%H%M%S%6N",
  11. time: "%H:%M",
  12. short: "%d %b %H:%M",
  13. long: "%B %d, %Y %H:%M",
  14. long_ordinal: lambda { |time|
  15. day_format = ActiveSupport::Inflector.ordinalize(time.day)
  16. time.strftime("%B #{day_format}, %Y %H:%M")
  17. },
  18. rfc822: lambda { |time|
  19. offset_format = time.formatted_offset(false)
  20. time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
  21. },
  22. iso8601: lambda { |time| time.iso8601 }
  23. }
  24. # Converts to a formatted string. See DATE_FORMATS for built-in formats.
  25. #
  26. # This method is aliased to <tt>to_s</tt>.
  27. #
  28. # time = Time.now # => 2007-01-18 06:10:17 -06:00
  29. #
  30. # time.to_formatted_s(:time) # => "06:10"
  31. # time.to_s(:time) # => "06:10"
  32. #
  33. # time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
  34. # time.to_formatted_s(:number) # => "20070118061017"
  35. # time.to_formatted_s(:short) # => "18 Jan 06:10"
  36. # time.to_formatted_s(:long) # => "January 18, 2007 06:10"
  37. # time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
  38. # time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
  39. # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00"
  40. #
  41. # == Adding your own time formats to +to_formatted_s+
  42. # You can add your own formats to the Time::DATE_FORMATS hash.
  43. # Use the format name as the hash key and either a strftime string
  44. # or Proc instance that takes a time argument as the value.
  45. #
  46. # # config/initializers/time_formats.rb
  47. # Time::DATE_FORMATS[:month_and_year] = '%B %Y'
  48. # Time::DATE_FORMATS[:short_ordinal] = ->(time) { time.strftime("%B #{time.day.ordinalize}") }
  49. 23 def to_formatted_s(format = :default)
  50. if formatter = DATE_FORMATS[format]
  51. formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  52. else
  53. to_default_s
  54. end
  55. end
  56. 23 alias_method :to_default_s, :to_s
  57. 23 alias_method :to_s, :to_formatted_s
  58. # Returns a formatted string of the offset from UTC, or an alternative
  59. # string if the time zone is already UTC.
  60. #
  61. # Time.local(2000).formatted_offset # => "-06:00"
  62. # Time.local(2000).formatted_offset(false) # => "-0600"
  63. 23 def formatted_offset(colon = true, alternate_utc_string = nil)
  64. utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
  65. end
  66. # Aliased to +xmlschema+ for compatibility with +DateTime+
  67. 23 alias_method :rfc3339, :xmlschema
  68. end

lib/active_support/core_ext/time/zones.rb

42.86% lines covered

28 relevant lines. 12 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/time_with_zone"
  3. 23 require "active_support/core_ext/time/acts_like"
  4. 23 require "active_support/core_ext/date_and_time/zones"
  5. 23 class Time
  6. 23 include DateAndTime::Zones
  7. 23 class << self
  8. 23 attr_accessor :zone_default
  9. # Returns the TimeZone for the current request, if this has been set (via Time.zone=).
  10. # If <tt>Time.zone</tt> has not been set for the current request, returns the TimeZone specified in <tt>config.time_zone</tt>.
  11. 23 def zone
  12. Thread.current[:time_zone] || zone_default
  13. end
  14. # Sets <tt>Time.zone</tt> to a TimeZone object for the current request/thread.
  15. #
  16. # This method accepts any of the following:
  17. #
  18. # * A Rails TimeZone object.
  19. # * An identifier for a Rails TimeZone object (e.g., "Eastern Time (US & Canada)", <tt>-5.hours</tt>).
  20. # * A TZInfo::Timezone object.
  21. # * An identifier for a TZInfo::Timezone object (e.g., "America/New_York").
  22. #
  23. # Here's an example of how you might set <tt>Time.zone</tt> on a per request basis and reset it when the request is done.
  24. # <tt>current_user.time_zone</tt> just needs to return a string identifying the user's preferred time zone:
  25. #
  26. # class ApplicationController < ActionController::Base
  27. # around_action :set_time_zone
  28. #
  29. # def set_time_zone
  30. # if logged_in?
  31. # Time.use_zone(current_user.time_zone) { yield }
  32. # else
  33. # yield
  34. # end
  35. # end
  36. # end
  37. 23 def zone=(time_zone)
  38. Thread.current[:time_zone] = find_zone!(time_zone)
  39. end
  40. # Allows override of <tt>Time.zone</tt> locally inside supplied block;
  41. # resets <tt>Time.zone</tt> to existing value when done.
  42. #
  43. # class ApplicationController < ActionController::Base
  44. # around_action :set_time_zone
  45. #
  46. # private
  47. #
  48. # def set_time_zone
  49. # Time.use_zone(current_user.timezone) { yield }
  50. # end
  51. # end
  52. #
  53. # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
  54. # objects that have already been created, e.g. any model timestamp
  55. # attributes that have been read before the block will remain in
  56. # the application's default timezone.
  57. 23 def use_zone(time_zone)
  58. new_zone = find_zone!(time_zone)
  59. begin
  60. old_zone, ::Time.zone = ::Time.zone, new_zone
  61. yield
  62. ensure
  63. ::Time.zone = old_zone
  64. end
  65. end
  66. # Returns a TimeZone instance matching the time zone provided.
  67. # Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
  68. # Raises an +ArgumentError+ for invalid time zones.
  69. #
  70. # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
  71. # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...>
  72. # Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...>
  73. # Time.find_zone! nil # => nil
  74. # Time.find_zone! false # => false
  75. # Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE
  76. 23 def find_zone!(time_zone)
  77. if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone)
  78. time_zone
  79. else
  80. # Look up the timezone based on the identifier (unless we've been
  81. # passed a TZInfo::Timezone)
  82. unless time_zone.respond_to?(:period_for_local)
  83. time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone)
  84. end
  85. # Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone
  86. if time_zone.is_a?(ActiveSupport::TimeZone)
  87. time_zone
  88. else
  89. ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone)
  90. end
  91. end
  92. rescue TZInfo::InvalidTimezoneIdentifier
  93. raise ArgumentError, "Invalid Timezone: #{time_zone}"
  94. end
  95. # Returns a TimeZone instance matching the time zone provided.
  96. # Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
  97. # Returns +nil+ for invalid time zones.
  98. #
  99. # Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
  100. # Time.find_zone "NOT-A-TIMEZONE" # => nil
  101. 23 def find_zone(time_zone)
  102. find_zone!(time_zone) rescue nil
  103. end
  104. end
  105. end

lib/active_support/core_ext/uri.rb

64.29% lines covered

14 relevant lines. 9 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "uri"
  3. 1 if RUBY_VERSION < "2.6.0"
  4. 1 require "active_support/core_ext/module/redefine_method"
  5. 1 URI::Parser.class_eval do
  6. 1 silence_redefinition_of_method :unescape
  7. 1 def unescape(str, escaped = /%[a-fA-F\d]{2}/)
  8. # TODO: Are we actually sure that ASCII == UTF-8?
  9. # YK: My initial experiments say yes, but let's be sure please
  10. enc = str.encoding
  11. enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
  12. str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
  13. end
  14. end
  15. end
  16. 1 module URI
  17. 1 class << self
  18. 1 def parser
  19. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  20. URI.parser is deprecated and will be removed in Rails 6.2.
  21. Use `URI::DEFAULT_PARSER` instead.
  22. MSG
  23. URI::DEFAULT_PARSER
  24. end
  25. end
  26. end

lib/active_support/current_attributes.rb

70.69% lines covered

58 relevant lines. 41 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/callbacks"
  3. 1 require "active_support/core_ext/enumerable"
  4. 1 module ActiveSupport
  5. # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
  6. # before and after each request. This allows you to keep all the per-request attributes easily
  7. # available to the whole system.
  8. #
  9. # The following full app-like example demonstrates how to use a Current class to
  10. # facilitate easy access to the global, per-request attributes without passing them deeply
  11. # around everywhere:
  12. #
  13. # # app/models/current.rb
  14. # class Current < ActiveSupport::CurrentAttributes
  15. # attribute :account, :user
  16. # attribute :request_id, :user_agent, :ip_address
  17. #
  18. # resets { Time.zone = nil }
  19. #
  20. # def user=(user)
  21. # super
  22. # self.account = user.account
  23. # Time.zone = user.time_zone
  24. # end
  25. # end
  26. #
  27. # # app/controllers/concerns/authentication.rb
  28. # module Authentication
  29. # extend ActiveSupport::Concern
  30. #
  31. # included do
  32. # before_action :authenticate
  33. # end
  34. #
  35. # private
  36. # def authenticate
  37. # if authenticated_user = User.find_by(id: cookies.encrypted[:user_id])
  38. # Current.user = authenticated_user
  39. # else
  40. # redirect_to new_session_url
  41. # end
  42. # end
  43. # end
  44. #
  45. # # app/controllers/concerns/set_current_request_details.rb
  46. # module SetCurrentRequestDetails
  47. # extend ActiveSupport::Concern
  48. #
  49. # included do
  50. # before_action do
  51. # Current.request_id = request.uuid
  52. # Current.user_agent = request.user_agent
  53. # Current.ip_address = request.ip
  54. # end
  55. # end
  56. # end
  57. #
  58. # class ApplicationController < ActionController::Base
  59. # include Authentication
  60. # include SetCurrentRequestDetails
  61. # end
  62. #
  63. # class MessagesController < ApplicationController
  64. # def create
  65. # Current.account.messages.create(message_params)
  66. # end
  67. # end
  68. #
  69. # class Message < ApplicationRecord
  70. # belongs_to :creator, default: -> { Current.user }
  71. # after_create { |message| Event.create(record: message) }
  72. # end
  73. #
  74. # class Event < ApplicationRecord
  75. # before_create do
  76. # self.request_id = Current.request_id
  77. # self.user_agent = Current.user_agent
  78. # self.ip_address = Current.ip_address
  79. # end
  80. # end
  81. #
  82. # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
  83. # Current should only be used for a few, top-level globals, like account, user, and request details.
  84. # The attributes stuck in Current should be used by more or less all actions on all requests. If you start
  85. # sticking controller-specific attributes in there, you're going to create a mess.
  86. 1 class CurrentAttributes
  87. 1 include ActiveSupport::Callbacks
  88. 1 define_callbacks :reset
  89. 1 class << self
  90. # Returns singleton instance for this class in this thread. If none exists, one is created.
  91. 1 def instance
  92. 2 current_instances[current_instances_key] ||= new
  93. end
  94. # Declares one or more attributes that will be given both class and instance accessor methods.
  95. 1 def attribute(*names)
  96. 2 generated_attribute_methods.module_eval do
  97. 2 names.each do |name|
  98. 6 define_method(name) do
  99. attributes[name.to_sym]
  100. end
  101. 6 define_method("#{name}=") do |attribute|
  102. attributes[name.to_sym] = attribute
  103. end
  104. end
  105. end
  106. 2 names.each do |name|
  107. 6 define_singleton_method(name) do
  108. instance.public_send(name)
  109. end
  110. 6 define_singleton_method("#{name}=") do |attribute|
  111. instance.public_send("#{name}=", attribute)
  112. end
  113. end
  114. end
  115. # Calls this block before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
  116. 1 def before_reset(&block)
  117. 1 set_callback :reset, :before, &block
  118. end
  119. # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
  120. 1 def resets(&block)
  121. 1 set_callback :reset, :after, &block
  122. end
  123. 1 alias_method :after_reset, :resets
  124. 1 delegate :set, :reset, to: :instance
  125. 1 def reset_all # :nodoc:
  126. current_instances.each_value(&:reset)
  127. end
  128. 1 def clear_all # :nodoc:
  129. reset_all
  130. current_instances.clear
  131. end
  132. 1 private
  133. 1 def generated_attribute_methods
  134. 4 @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
  135. end
  136. 1 def current_instances
  137. 2 Thread.current[:current_attributes_instances] ||= {}
  138. end
  139. 1 def current_instances_key
  140. 2 @current_instances_key ||= name.to_sym
  141. end
  142. 1 def method_missing(name, *args, &block)
  143. # Caches the method definition as a singleton method of the receiver.
  144. #
  145. # By letting #delegate handle it, we avoid an enclosure that'll capture args.
  146. singleton_class.delegate name, to: :instance
  147. send(name, *args, &block)
  148. end
  149. end
  150. 1 attr_accessor :attributes
  151. 1 def initialize
  152. 2 @attributes = {}
  153. end
  154. # Expose one or more attributes within a block. Old values are returned after the block concludes.
  155. # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
  156. #
  157. # class Chat::PublicationJob < ApplicationJob
  158. # def perform(attributes, room_number, creator)
  159. # Current.set(person: creator) do
  160. # Chat::Publisher.publish(attributes: attributes, room_number: room_number)
  161. # end
  162. # end
  163. # end
  164. 1 def set(set_attributes)
  165. old_attributes = compute_attributes(set_attributes.keys)
  166. assign_attributes(set_attributes)
  167. yield
  168. ensure
  169. assign_attributes(old_attributes)
  170. end
  171. # Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
  172. 1 def reset
  173. run_callbacks :reset do
  174. self.attributes = {}
  175. end
  176. end
  177. 1 private
  178. 1 def assign_attributes(new_attributes)
  179. new_attributes.each { |key, value| public_send("#{key}=", value) }
  180. end
  181. 1 def compute_attributes(keys)
  182. keys.index_with { |key| public_send(key) }
  183. end
  184. end
  185. end

lib/active_support/current_attributes/test_helper.rb

42.86% lines covered

7 relevant lines. 3 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
  3. 1 def before_setup
  4. ActiveSupport::CurrentAttributes.reset_all
  5. super
  6. end
  7. 1 def before_teardown
  8. ActiveSupport::CurrentAttributes.reset_all
  9. super
  10. end
  11. end

lib/active_support/dependencies.rb

38.07% lines covered

352 relevant lines. 134 lines covered and 218 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "set"
  3. 3 require "thread"
  4. 3 require "concurrent/map"
  5. 3 require "pathname"
  6. 3 require "active_support/core_ext/module/aliasing"
  7. 3 require "active_support/core_ext/module/attribute_accessors"
  8. 3 require "active_support/core_ext/module/introspection"
  9. 3 require "active_support/core_ext/module/anonymous"
  10. 3 require "active_support/core_ext/object/blank"
  11. 3 require "active_support/core_ext/kernel/reporting"
  12. 3 require "active_support/core_ext/load_error"
  13. 3 require "active_support/core_ext/name_error"
  14. 3 require "active_support/dependencies/interlock"
  15. 3 require "active_support/inflector"
  16. 3 module ActiveSupport #:nodoc:
  17. 3 module Dependencies #:nodoc:
  18. 3 extend self
  19. 3 UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
  20. 3 private_constant :UNBOUND_METHOD_MODULE_NAME
  21. 3 mattr_accessor :interlock, default: Interlock.new
  22. # :doc:
  23. # Execute the supplied block without interference from any
  24. # concurrent loads.
  25. 3 def self.run_interlock
  26. Dependencies.interlock.running { yield }
  27. end
  28. # Execute the supplied block while holding an exclusive lock,
  29. # preventing any other thread from being inside a #run_interlock
  30. # block at the same time.
  31. 3 def self.load_interlock
  32. Dependencies.interlock.loading { yield }
  33. end
  34. # Execute the supplied block while holding an exclusive lock,
  35. # preventing any other thread from being inside a #run_interlock
  36. # block at the same time.
  37. 3 def self.unload_interlock
  38. Dependencies.interlock.unloading { yield }
  39. end
  40. # :nodoc:
  41. # Should we turn on Ruby warnings on the first load of dependent files?
  42. 3 mattr_accessor :warnings_on_first_load, default: false
  43. # All files ever loaded.
  44. 3 mattr_accessor :history, default: Set.new
  45. # All files currently loaded.
  46. 3 mattr_accessor :loaded, default: Set.new
  47. # Stack of files being loaded.
  48. 3 mattr_accessor :loading, default: []
  49. # Should we load files or require them?
  50. 3 mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load
  51. # The set of directories from which we may automatically load files. Files
  52. # under these directories will be reloaded on each request in development mode,
  53. # unless the directory also appears in autoload_once_paths.
  54. 3 mattr_accessor :autoload_paths, default: []
  55. # The set of directories from which automatically loaded constants are loaded
  56. # only once. All directories in this set must also be present in +autoload_paths+.
  57. 3 mattr_accessor :autoload_once_paths, default: []
  58. # This is a private set that collects all eager load paths during bootstrap.
  59. # Useful for Zeitwerk integration. Its public interface is the config.* path
  60. # accessors of each engine.
  61. 3 mattr_accessor :_eager_load_paths, default: Set.new
  62. # An array of qualified constant names that have been loaded. Adding a name
  63. # to this array will cause it to be unloaded the next time Dependencies are
  64. # cleared.
  65. 3 mattr_accessor :autoloaded_constants, default: []
  66. # An array of constant names that need to be unloaded on every request. Used
  67. # to allow arbitrary constants to be marked for unloading.
  68. 3 mattr_accessor :explicitly_unloadable_constants, default: []
  69. # The logger used when tracing autoloads.
  70. 3 mattr_accessor :logger
  71. # If true, trace autoloads with +logger.debug+.
  72. 3 mattr_accessor :verbose, default: false
  73. # The WatchStack keeps a stack of the modules being watched as files are
  74. # loaded. If a file in the process of being loaded (parent.rb) triggers the
  75. # load of another file (child.rb) the stack will ensure that child.rb
  76. # handles the new constants.
  77. #
  78. # If child.rb is being autoloaded, its constants will be added to
  79. # autoloaded_constants. If it was being required, they will be discarded.
  80. #
  81. # This is handled by walking back up the watch stack and adding the constants
  82. # found by child.rb to the list of original constants in parent.rb.
  83. 3 class WatchStack
  84. 3 include Enumerable
  85. # @watching is a stack of lists of constants being watched. For instance,
  86. # if parent.rb is autoloaded, the stack will look like [[Object]]. If
  87. # parent.rb then requires namespace/child.rb, the stack will look like
  88. # [[Object], [Namespace]].
  89. 3 attr_reader :watching
  90. 3 def initialize
  91. 3 @watching = []
  92. 3 @stack = Hash.new { |h, k| h[k] = [] }
  93. end
  94. 3 def each(&block)
  95. @stack.each(&block)
  96. end
  97. 3 def watching?
  98. !@watching.empty?
  99. end
  100. # Returns a list of new constants found since the last call to
  101. # <tt>watch_namespaces</tt>.
  102. 3 def new_constants
  103. constants = []
  104. # Grab the list of namespaces that we're looking for new constants under
  105. @watching.last.each do |namespace|
  106. # Retrieve the constants that were present under the namespace when watch_namespaces
  107. # was originally called
  108. original_constants = @stack[namespace].last
  109. mod = Inflector.constantize(namespace) if Dependencies.qualified_const_defined?(namespace)
  110. next unless mod.is_a?(Module)
  111. # Get a list of the constants that were added
  112. new_constants = mod.constants(false) - original_constants
  113. # @stack[namespace] returns an Array of the constants that are being evaluated
  114. # for that namespace. For instance, if parent.rb requires child.rb, the first
  115. # element of @stack[Object] will be an Array of the constants that were present
  116. # before parent.rb was required. The second element will be an Array of the
  117. # constants that were present before child.rb was required.
  118. @stack[namespace].each do |namespace_constants|
  119. namespace_constants.concat(new_constants)
  120. end
  121. # Normalize the list of new constants, and add them to the list we will return
  122. new_constants.each do |suffix|
  123. constants << ([namespace, suffix] - ["Object"]).join("::")
  124. end
  125. end
  126. constants
  127. ensure
  128. # A call to new_constants is always called after a call to watch_namespaces
  129. pop_modules(@watching.pop)
  130. end
  131. # Add a set of modules to the watch stack, remembering the initial
  132. # constants.
  133. 3 def watch_namespaces(namespaces)
  134. @watching << namespaces.map do |namespace|
  135. module_name = Dependencies.to_constant_name(namespace)
  136. original_constants = Dependencies.qualified_const_defined?(module_name) ?
  137. Inflector.constantize(module_name).constants(false) : []
  138. @stack[module_name] << original_constants
  139. module_name
  140. end
  141. end
  142. 3 private
  143. 3 def pop_modules(modules)
  144. modules.each { |mod| @stack[mod].pop }
  145. end
  146. end
  147. # An internal stack used to record which constants are loaded by any block.
  148. 3 mattr_accessor :constant_watch_stack, default: WatchStack.new
  149. # Module includes this module.
  150. 3 module ModuleConstMissing #:nodoc:
  151. 3 def self.append_features(base)
  152. 6 base.class_eval do
  153. # Emulate #exclude via an ivar
  154. 6 return if defined?(@_const_missing) && @_const_missing
  155. 3 @_const_missing = instance_method(:const_missing)
  156. 3 remove_method(:const_missing)
  157. end
  158. 3 super
  159. end
  160. 3 def self.exclude_from(base)
  161. base.class_eval do
  162. define_method :const_missing, @_const_missing
  163. @_const_missing = nil
  164. end
  165. end
  166. 3 def self.include_into(base)
  167. 3 base.include(self)
  168. 3 append_features(base)
  169. end
  170. 3 def const_missing(const_name)
  171. from_mod = anonymous? ? guess_for_anonymous(const_name) : self
  172. Dependencies.load_missing_constant(from_mod, const_name)
  173. end
  174. # We assume that the name of the module reflects the nesting
  175. # (unless it can be proven that is not the case) and the path to the file
  176. # that defines the constant. Anonymous modules cannot follow these
  177. # conventions and therefore we assume that the user wants to refer to a
  178. # top-level constant.
  179. 3 def guess_for_anonymous(const_name)
  180. if Object.const_defined?(const_name)
  181. raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
  182. else
  183. Object
  184. end
  185. end
  186. 3 def unloadable(const_desc = self)
  187. super(const_desc)
  188. end
  189. end
  190. # Object includes this module.
  191. 3 module Loadable #:nodoc:
  192. 3 def self.exclude_from(base)
  193. base.class_eval do
  194. define_method(:load, Kernel.instance_method(:load))
  195. private :load
  196. define_method(:require, Kernel.instance_method(:require))
  197. private :require
  198. end
  199. end
  200. 3 def self.include_into(base)
  201. 3 base.include(self)
  202. 3 if base.instance_method(:load).owner == base
  203. base.remove_method(:load)
  204. end
  205. 3 if base.instance_method(:require).owner == base
  206. base.remove_method(:require)
  207. end
  208. end
  209. 3 def require_or_load(file_name)
  210. Dependencies.require_or_load(file_name)
  211. end
  212. # :doc:
  213. # <b>Warning:</b> This method is obsolete in +:zeitwerk+ mode. In
  214. # +:zeitwerk+ mode semantics match Ruby's and you do not need to be
  215. # defensive with load order. Just refer to classes and modules normally.
  216. # If the constant name is dynamic, camelize if needed, and constantize.
  217. #
  218. # In +:classic+ mode, interprets a file using +mechanism+ and marks its
  219. # defined constants as autoloaded. +file_name+ can be either a string or
  220. # respond to <tt>to_path</tt>.
  221. #
  222. # In +:classic+ mode, use this method in code that absolutely needs a
  223. # certain constant to be defined at that point. A typical use case is to
  224. # make constant name resolution deterministic for constants with the same
  225. # relative name in different namespaces whose evaluation would depend on
  226. # load order otherwise.
  227. #
  228. # Engines that do not control the mode in which their parent application
  229. # runs should call +require_dependency+ where needed in case the runtime
  230. # mode is +:classic+.
  231. 3 def require_dependency(file_name, message = "No such file to load -- %s.rb")
  232. file_name = file_name.to_path if file_name.respond_to?(:to_path)
  233. unless file_name.is_a?(String)
  234. raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
  235. end
  236. Dependencies.depend_on(file_name, message)
  237. end
  238. # :nodoc:
  239. 3 def load_dependency(file)
  240. 767 if Dependencies.load? && Dependencies.constant_watch_stack.watching?
  241. descs = Dependencies.constant_watch_stack.watching.flatten.uniq
  242. Dependencies.new_constants_in(*descs) { yield }
  243. else
  244. 767 yield
  245. end
  246. rescue Exception => exception # errors from loading file
  247. 4 exception.blame_file! file if exception.respond_to? :blame_file!
  248. 4 raise
  249. end
  250. # Mark the given constant as unloadable. Unloadable constants are removed
  251. # each time dependencies are cleared.
  252. #
  253. # Note that marking a constant for unloading need only be done once. Setup
  254. # or init scripts may list each unloadable constant that may need unloading;
  255. # each constant will be removed for every subsequent clear, as opposed to
  256. # for the first clear.
  257. #
  258. # The provided constant descriptor may be a (non-anonymous) module or class,
  259. # or a qualified constant name as a string or symbol.
  260. #
  261. # Returns +true+ if the constant was not previously marked for unloading,
  262. # +false+ otherwise.
  263. 3 def unloadable(const_desc)
  264. Dependencies.mark_for_unload const_desc
  265. end
  266. 3 private
  267. 3 def load(file, wrap = false)
  268. result = false
  269. load_dependency(file) { result = super }
  270. result
  271. end
  272. 3 def require(file)
  273. 767 result = false
  274. 1534 load_dependency(file) { result = super }
  275. 763 result
  276. end
  277. end
  278. # Exception file-blaming.
  279. 3 module Blamable #:nodoc:
  280. 3 def blame_file!(file)
  281. 4 (@blamed_files ||= []).unshift file
  282. end
  283. 3 def blamed_files
  284. @blamed_files ||= []
  285. end
  286. 3 def describe_blame
  287. return nil if blamed_files.empty?
  288. "This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
  289. end
  290. 3 def copy_blame!(exc)
  291. @blamed_files = exc.blamed_files.clone
  292. self
  293. end
  294. end
  295. 3 def hook!
  296. 3 Loadable.include_into(Object)
  297. 3 ModuleConstMissing.include_into(Module)
  298. 3 Exception.include(Blamable)
  299. end
  300. 3 def unhook!
  301. ModuleConstMissing.exclude_from(Module)
  302. Loadable.exclude_from(Object)
  303. end
  304. 3 def load?
  305. 767 mechanism == :load
  306. end
  307. 3 def depend_on(file_name, message = "No such file to load -- %s.rb")
  308. path = search_for_file(file_name)
  309. require_or_load(path || file_name)
  310. rescue LoadError => load_error
  311. if file_name = load_error.message[/ -- (.*?)(\.rb)?$/, 1]
  312. load_error.message.replace(message % file_name)
  313. load_error.copy_blame!(load_error)
  314. end
  315. raise
  316. end
  317. 3 def clear
  318. Dependencies.unload_interlock do
  319. loaded.clear
  320. loading.clear
  321. remove_unloadable_constants!
  322. end
  323. end
  324. 3 def require_or_load(file_name, const_path = nil)
  325. file_name = file_name.chomp(".rb")
  326. expanded = File.expand_path(file_name)
  327. return if loaded.include?(expanded)
  328. Dependencies.load_interlock do
  329. # Maybe it got loaded while we were waiting for our lock:
  330. return if loaded.include?(expanded)
  331. # Record that we've seen this file *before* loading it to avoid an
  332. # infinite loop with mutual dependencies.
  333. loaded << expanded
  334. loading << expanded
  335. begin
  336. if load?
  337. # Enable warnings if this file has not been loaded before and
  338. # warnings_on_first_load is set.
  339. load_args = ["#{file_name}.rb"]
  340. load_args << const_path unless const_path.nil?
  341. if !warnings_on_first_load || history.include?(expanded)
  342. result = load_file(*load_args)
  343. else
  344. enable_warnings { result = load_file(*load_args) }
  345. end
  346. else
  347. result = require file_name
  348. end
  349. rescue Exception
  350. loaded.delete expanded
  351. raise
  352. ensure
  353. loading.pop
  354. end
  355. # Record history *after* loading so first load gets warnings.
  356. history << expanded
  357. result
  358. end
  359. end
  360. # Is the provided constant path defined?
  361. 3 def qualified_const_defined?(path)
  362. Object.const_defined?(path, false)
  363. end
  364. # Given +path+, a filesystem path to a ruby file, return an array of
  365. # constant paths which would cause Dependencies to attempt to load this
  366. # file.
  367. 3 def loadable_constants_for_path(path, bases = autoload_paths)
  368. path = path.chomp(".rb")
  369. expanded_path = File.expand_path(path)
  370. paths = []
  371. bases.each do |root|
  372. expanded_root = File.expand_path(root)
  373. next unless expanded_path.start_with?(expanded_root)
  374. root_size = expanded_root.size
  375. next if expanded_path[root_size] != ?/
  376. nesting = expanded_path[(root_size + 1)..-1]
  377. paths << nesting.camelize unless nesting.blank?
  378. end
  379. paths.uniq!
  380. paths
  381. end
  382. # Search for a file in autoload_paths matching the provided suffix.
  383. 3 def search_for_file(path_suffix)
  384. path_suffix += ".rb" unless path_suffix.end_with?(".rb")
  385. autoload_paths.each do |root|
  386. path = File.join(root, path_suffix)
  387. return path if File.file? path
  388. end
  389. nil # Gee, I sure wish we had first_match ;-)
  390. end
  391. # Does the provided path_suffix correspond to an autoloadable module?
  392. # Instead of returning a boolean, the autoload base for this module is
  393. # returned.
  394. 3 def autoloadable_module?(path_suffix)
  395. autoload_paths.each do |load_path|
  396. return load_path if File.directory? File.join(load_path, path_suffix)
  397. end
  398. nil
  399. end
  400. 3 def load_once_path?(path)
  401. # to_s works around a ruby issue where String#start_with?(Pathname)
  402. # will raise a TypeError: no implicit conversion of Pathname into String
  403. autoload_once_paths.any? { |base| path.start_with?(base.to_s) }
  404. end
  405. # Attempt to autoload the provided module name by searching for a directory
  406. # matching the expected path suffix. If found, the module is created and
  407. # assigned to +into+'s constants with the name +const_name+. Provided that
  408. # the directory was loaded from a reloadable base path, it is added to the
  409. # set of constants that are to be unloaded.
  410. 3 def autoload_module!(into, const_name, qualified_name, path_suffix)
  411. return nil unless base_path = autoloadable_module?(path_suffix)
  412. mod = Module.new
  413. into.const_set const_name, mod
  414. log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})")
  415. autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
  416. autoloaded_constants.uniq!
  417. mod
  418. end
  419. # Load the file at the provided path. +const_paths+ is a set of qualified
  420. # constant names. When loading the file, Dependencies will watch for the
  421. # addition of these constants. Each that is defined will be marked as
  422. # autoloaded, and will be removed when Dependencies.clear is next called.
  423. #
  424. # If the second parameter is left off, then Dependencies will construct a
  425. # set of names that the file at +path+ may define. See
  426. # +loadable_constants_for_path+ for more details.
  427. 3 def load_file(path, const_paths = loadable_constants_for_path(path))
  428. const_paths = [const_paths].compact unless const_paths.is_a? Array
  429. parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
  430. result = nil
  431. newly_defined_paths = new_constants_in(*parent_paths) do
  432. result = Kernel.load path
  433. end
  434. autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
  435. autoloaded_constants.uniq!
  436. result
  437. end
  438. # Returns the constant path for the provided parent and constant name.
  439. 3 def qualified_name_for(mod, name)
  440. mod_name = to_constant_name mod
  441. mod_name == "Object" ? name.to_s : "#{mod_name}::#{name}"
  442. end
  443. # Load the constant named +const_name+ which is missing from +from_mod+. If
  444. # it is not possible to load the constant into from_mod, try its parent
  445. # module using +const_missing+.
  446. 3 def load_missing_constant(from_mod, const_name)
  447. unless qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod)
  448. raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
  449. end
  450. qualified_name = qualified_name_for(from_mod, const_name)
  451. path_suffix = qualified_name.underscore
  452. file_path = search_for_file(path_suffix)
  453. if file_path
  454. expanded = File.expand_path(file_path)
  455. expanded.delete_suffix!(".rb")
  456. if loading.include?(expanded)
  457. raise "Circular dependency detected while autoloading constant #{qualified_name}"
  458. else
  459. require_or_load(expanded, qualified_name)
  460. if from_mod.const_defined?(const_name, false)
  461. log("constant #{qualified_name} autoloaded from #{expanded}.rb")
  462. return from_mod.const_get(const_name)
  463. else
  464. raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it"
  465. end
  466. end
  467. elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
  468. return mod
  469. elsif (parent = from_mod.module_parent) && parent != from_mod &&
  470. ! from_mod.module_parents.any? { |p| p.const_defined?(const_name, false) }
  471. # If our parents do not have a constant named +const_name+ then we are free
  472. # to attempt to load upwards. If they do have such a constant, then this
  473. # const_missing must be due to from_mod::const_name, which should not
  474. # return constants from from_mod's parents.
  475. begin
  476. # Since Ruby does not pass the nesting at the point the unknown
  477. # constant triggered the callback we cannot fully emulate constant
  478. # name lookup and need to make a trade-off: we are going to assume
  479. # that the nesting in the body of Foo::Bar is [Foo::Bar, Foo] even
  480. # though it might not be. Counterexamples are
  481. #
  482. # class Foo::Bar
  483. # Module.nesting # => [Foo::Bar]
  484. # end
  485. #
  486. # or
  487. #
  488. # module M::N
  489. # module S::T
  490. # Module.nesting # => [S::T, M::N]
  491. # end
  492. # end
  493. #
  494. # for example.
  495. return parent.const_missing(const_name)
  496. rescue NameError => e
  497. raise unless e.missing_name? qualified_name_for(parent, const_name)
  498. end
  499. end
  500. name_error = uninitialized_constant(qualified_name, const_name, receiver: from_mod)
  501. name_error.set_backtrace(caller.reject { |l| l.start_with? __FILE__ })
  502. raise name_error
  503. end
  504. # Remove the constants that have been autoloaded, and those that have been
  505. # marked for unloading. Before each constant is removed a callback is sent
  506. # to its class/module if it implements +before_remove_const+.
  507. #
  508. # The callback implementation should be restricted to cleaning up caches, etc.
  509. # as the environment will be in an inconsistent state, e.g. other constants
  510. # may have already been unloaded and not accessible.
  511. 3 def remove_unloadable_constants!
  512. log("removing unloadable constants")
  513. autoloaded_constants.each { |const| remove_constant const }
  514. autoloaded_constants.clear
  515. Reference.clear!
  516. explicitly_unloadable_constants.each { |const| remove_constant const }
  517. end
  518. 3 class ClassCache
  519. 3 def initialize
  520. 3 @store = Concurrent::Map.new
  521. end
  522. 3 def empty?
  523. @store.empty?
  524. end
  525. 3 def key?(key)
  526. @store.key?(key)
  527. end
  528. 3 def get(key)
  529. key = key.name if key.respond_to?(:name)
  530. @store[key] ||= Inflector.constantize(key)
  531. end
  532. 3 alias :[] :get
  533. 3 def safe_get(key)
  534. key = key.name if key.respond_to?(:name)
  535. @store[key] ||= Inflector.safe_constantize(key)
  536. end
  537. 3 def store(klass)
  538. return self unless klass.respond_to?(:name)
  539. raise(ArgumentError, "anonymous classes cannot be cached") if klass.name.empty?
  540. @store[klass.name] = klass
  541. self
  542. end
  543. 3 def clear!
  544. @store.clear
  545. end
  546. end
  547. 3 Reference = ClassCache.new
  548. # Store a reference to a class +klass+.
  549. 3 def reference(klass)
  550. Reference.store klass
  551. end
  552. # Get the reference for class named +name+.
  553. # Raises an exception if referenced class does not exist.
  554. 3 def constantize(name)
  555. Reference.get(name)
  556. end
  557. # Get the reference for class named +name+ if one exists.
  558. # Otherwise returns +nil+.
  559. 3 def safe_constantize(name)
  560. Reference.safe_get(name)
  561. end
  562. # Determine if the given constant has been automatically loaded.
  563. 3 def autoloaded?(desc)
  564. return false if desc.is_a?(Module) && real_mod_name(desc).nil?
  565. name = to_constant_name desc
  566. return false unless qualified_const_defined?(name)
  567. autoloaded_constants.include?(name)
  568. end
  569. # Will the provided constant descriptor be unloaded?
  570. 3 def will_unload?(const_desc)
  571. autoloaded?(const_desc) ||
  572. explicitly_unloadable_constants.include?(to_constant_name(const_desc))
  573. end
  574. # Mark the provided constant name for unloading. This constant will be
  575. # unloaded on each request, not just the next one.
  576. 3 def mark_for_unload(const_desc)
  577. name = to_constant_name const_desc
  578. if explicitly_unloadable_constants.include? name
  579. false
  580. else
  581. explicitly_unloadable_constants << name
  582. true
  583. end
  584. end
  585. # Run the provided block and detect the new constants that were loaded during
  586. # its execution. Constants may only be regarded as 'new' once -- so if the
  587. # block calls +new_constants_in+ again, then the constants defined within the
  588. # inner call will not be reported in this one.
  589. #
  590. # If the provided block does not run to completion, and instead raises an
  591. # exception, any new constants are regarded as being only partially defined
  592. # and will be removed immediately.
  593. 3 def new_constants_in(*descs)
  594. constant_watch_stack.watch_namespaces(descs)
  595. success = false
  596. begin
  597. yield # Now yield to the code that is to define new constants.
  598. success = true
  599. ensure
  600. new_constants = constant_watch_stack.new_constants
  601. return new_constants if success
  602. # Remove partially loaded constants.
  603. new_constants.each { |c| remove_constant(c) }
  604. end
  605. end
  606. # Convert the provided const desc to a qualified constant name (as a string).
  607. # A module, class, symbol, or string may be provided.
  608. 3 def to_constant_name(desc) #:nodoc:
  609. case desc
  610. when String then desc.delete_prefix("::")
  611. when Symbol then desc.to_s
  612. when Module
  613. real_mod_name(desc) ||
  614. raise(ArgumentError, "Anonymous modules have no name to be referenced by")
  615. else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
  616. end
  617. end
  618. 3 def remove_constant(const) #:nodoc:
  619. # Normalize ::Foo, ::Object::Foo, Object::Foo, Object::Object::Foo, etc. as Foo.
  620. normalized = const.to_s.delete_prefix("::")
  621. normalized.sub!(/\A(Object::)+/, "")
  622. constants = normalized.split("::")
  623. to_remove = constants.pop
  624. # Remove the file path from the loaded list.
  625. file_path = search_for_file(const.underscore)
  626. if file_path
  627. expanded = File.expand_path(file_path)
  628. expanded.delete_suffix!(".rb")
  629. loaded.delete(expanded)
  630. end
  631. if constants.empty?
  632. parent = Object
  633. else
  634. # This method is robust to non-reachable constants.
  635. #
  636. # Non-reachable constants may be passed if some of the parents were
  637. # autoloaded and already removed. It is easier to do a sanity check
  638. # here than require the caller to be clever. We check the parent
  639. # rather than the very const argument because we do not want to
  640. # trigger Kernel#autoloads, see the comment below.
  641. parent_name = constants.join("::")
  642. return unless qualified_const_defined?(parent_name)
  643. parent = constantize(parent_name)
  644. end
  645. # In an autoloaded user.rb like this
  646. #
  647. # autoload :Foo, 'foo'
  648. #
  649. # class User < ActiveRecord::Base
  650. # end
  651. #
  652. # we correctly register "Foo" as being autoloaded. But if the app does
  653. # not use the "Foo" constant we need to be careful not to trigger
  654. # loading "foo.rb" ourselves. While #const_defined? and #const_get? do
  655. # require the file, #autoload? and #remove_const don't.
  656. #
  657. # We are going to remove the constant nonetheless ---which exists as
  658. # far as Ruby is concerned--- because if the user removes the macro
  659. # call from a class or module that were not autoloaded, as in the
  660. # example above with Object, accessing to that constant must err.
  661. unless parent.autoload?(to_remove)
  662. begin
  663. constantized = parent.const_get(to_remove, false)
  664. rescue NameError
  665. # The constant is no longer reachable, just skip it.
  666. return
  667. else
  668. constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
  669. end
  670. end
  671. begin
  672. parent.instance_eval { remove_const to_remove }
  673. rescue NameError
  674. # The constant is no longer reachable, just skip it.
  675. end
  676. end
  677. 3 def log(message)
  678. logger.debug("autoloading: #{message}") if logger && verbose
  679. end
  680. 3 private
  681. 3 if RUBY_VERSION < "2.6"
  682. 3 def uninitialized_constant(qualified_name, const_name, receiver:)
  683. NameError.new("uninitialized constant #{qualified_name}", const_name)
  684. end
  685. else
  686. def uninitialized_constant(qualified_name, const_name, receiver:)
  687. NameError.new("uninitialized constant #{qualified_name}", const_name, receiver: receiver)
  688. end
  689. end
  690. # Returns the original name of a class or module even if `name` has been
  691. # overridden.
  692. 3 def real_mod_name(mod)
  693. UNBOUND_METHOD_MODULE_NAME.bind(mod).call
  694. end
  695. end
  696. end
  697. 3 ActiveSupport::Dependencies.hook!

lib/active_support/dependencies/autoload.rb

75.0% lines covered

32 relevant lines. 24 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/inflector/methods"
  3. 24 module ActiveSupport
  4. # Autoload and eager load conveniences for your library.
  5. #
  6. # This module allows you to define autoloads based on
  7. # Rails conventions (i.e. no need to define the path
  8. # it is automatically guessed based on the filename)
  9. # and also define a set of constants that needs to be
  10. # eager loaded:
  11. #
  12. # module MyLib
  13. # extend ActiveSupport::Autoload
  14. #
  15. # autoload :Model
  16. #
  17. # eager_autoload do
  18. # autoload :Cache
  19. # end
  20. # end
  21. #
  22. # Then your library can be eager loaded by simply calling:
  23. #
  24. # MyLib.eager_load!
  25. 24 module Autoload
  26. 24 def self.extended(base) # :nodoc:
  27. 27 base.class_eval do
  28. 27 @_autoloads = {}
  29. 27 @_under_path = nil
  30. 27 @_at_path = nil
  31. 27 @_eager_autoload = false
  32. end
  33. end
  34. 24 def autoload(const_name, path = @_at_path)
  35. 1023 unless path
  36. 993 full = [name, @_under_path, const_name.to_s].compact.join("::")
  37. 993 path = Inflector.underscore(full)
  38. end
  39. 1023 if @_eager_autoload
  40. 585 @_autoloads[const_name] = path
  41. end
  42. 1023 super const_name, path
  43. end
  44. 24 def autoload_under(path)
  45. @_under_path, old_path = path, @_under_path
  46. yield
  47. ensure
  48. @_under_path = old_path
  49. end
  50. 24 def autoload_at(path)
  51. @_at_path, old_path = path, @_at_path
  52. yield
  53. ensure
  54. @_at_path = old_path
  55. end
  56. 24 def eager_autoload
  57. 25 old_eager, @_eager_autoload = @_eager_autoload, true
  58. 25 yield
  59. ensure
  60. 25 @_eager_autoload = old_eager
  61. end
  62. 24 def eager_load!
  63. @_autoloads.each_value { |file| require file }
  64. end
  65. 24 def autoloads
  66. @_autoloads
  67. end
  68. end
  69. end

lib/active_support/dependencies/interlock.rb

53.57% lines covered

28 relevant lines. 15 lines covered and 13 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/concurrency/share_lock"
  3. 3 module ActiveSupport #:nodoc:
  4. 3 module Dependencies #:nodoc:
  5. 3 class Interlock
  6. 3 def initialize # :nodoc:
  7. 3 @lock = ActiveSupport::Concurrency::ShareLock.new
  8. end
  9. 3 def loading
  10. @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do
  11. yield
  12. end
  13. end
  14. 3 def unloading
  15. @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do
  16. yield
  17. end
  18. end
  19. 3 def start_unloading
  20. @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
  21. end
  22. 3 def done_unloading
  23. @lock.stop_exclusive(compatible: [:load, :unload])
  24. end
  25. 3 def start_running
  26. @lock.start_sharing
  27. end
  28. 3 def done_running
  29. @lock.stop_sharing
  30. end
  31. 3 def running
  32. @lock.sharing do
  33. yield
  34. end
  35. end
  36. 3 def permit_concurrent_loads
  37. @lock.yield_shares(compatible: [:load]) do
  38. yield
  39. end
  40. end
  41. 3 def raw_state(&block) # :nodoc:
  42. @lock.raw_state(&block)
  43. end
  44. end
  45. end
  46. end

lib/active_support/dependencies/zeitwerk_integration.rb

42.86% lines covered

63 relevant lines. 27 lines covered and 36 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "set"
  3. 1 require "active_support/core_ext/string/inflections"
  4. 1 module ActiveSupport
  5. 1 module Dependencies
  6. 1 module ZeitwerkIntegration # :nodoc: all
  7. 1 module Decorations
  8. 1 def clear
  9. Dependencies.unload_interlock do
  10. Rails.autoloaders.main.reload
  11. rescue Zeitwerk::ReloadingDisabledError
  12. raise "reloading is disabled because config.cache_classes is true"
  13. end
  14. end
  15. 1 def constantize(cpath)
  16. ActiveSupport::Inflector.constantize(cpath)
  17. end
  18. 1 def safe_constantize(cpath)
  19. ActiveSupport::Inflector.safe_constantize(cpath)
  20. end
  21. 1 def autoloaded_constants
  22. Rails.autoloaders.main.unloadable_cpaths
  23. end
  24. 1 def autoloaded?(object)
  25. cpath = object.is_a?(Module) ? real_mod_name(object) : object.to_s
  26. Rails.autoloaders.main.unloadable_cpath?(cpath)
  27. end
  28. 1 def verbose=(verbose)
  29. l = verbose ? logger || Rails.logger : nil
  30. Rails.autoloaders.each { |autoloader| autoloader.logger = l }
  31. end
  32. 1 def unhook!
  33. :no_op
  34. end
  35. end
  36. 1 module RequireDependency
  37. 1 def require_dependency(filename)
  38. filename = filename.to_path if filename.respond_to?(:to_path)
  39. if abspath = ActiveSupport::Dependencies.search_for_file(filename)
  40. require abspath
  41. else
  42. require filename
  43. end
  44. end
  45. end
  46. 1 module Inflector
  47. # Concurrent::Map is not needed. This is a private class, and overrides
  48. # must be defined while the application boots.
  49. 1 @overrides = {}
  50. 1 def self.camelize(basename, _abspath)
  51. @overrides[basename] || basename.camelize
  52. end
  53. 1 def self.inflect(overrides)
  54. @overrides.merge!(overrides)
  55. end
  56. end
  57. 1 class << self
  58. 1 def take_over(enable_reloading:)
  59. setup_autoloaders(enable_reloading)
  60. freeze_paths
  61. decorate_dependencies
  62. end
  63. 1 private
  64. 1 def setup_autoloaders(enable_reloading)
  65. Dependencies.autoload_paths.each do |autoload_path|
  66. # Zeitwerk only accepts existing directories in `push_dir` to
  67. # prevent misconfigurations.
  68. next unless File.directory?(autoload_path)
  69. autoloader = \
  70. autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main
  71. autoloader.push_dir(autoload_path)
  72. autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
  73. end
  74. Rails.autoloaders.main.enable_reloading if enable_reloading
  75. Rails.autoloaders.each(&:setup)
  76. end
  77. 1 def autoload_once?(autoload_path)
  78. Dependencies.autoload_once_paths.include?(autoload_path)
  79. end
  80. 1 def eager_load?(autoload_path)
  81. Dependencies._eager_load_paths.member?(autoload_path)
  82. end
  83. 1 def freeze_paths
  84. Dependencies.autoload_paths.freeze
  85. Dependencies.autoload_once_paths.freeze
  86. Dependencies._eager_load_paths.freeze
  87. end
  88. 1 def decorate_dependencies
  89. Dependencies.unhook!
  90. Dependencies.singleton_class.prepend(Decorations)
  91. Object.prepend(RequireDependency)
  92. end
  93. end
  94. end
  95. end
  96. end

lib/active_support/deprecation.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "singleton"
  3. 24 module ActiveSupport
  4. # \Deprecation specifies the API used by Rails to deprecate methods, instance
  5. # variables, objects and constants.
  6. 24 class Deprecation
  7. # active_support.rb sets an autoload for ActiveSupport::Deprecation.
  8. #
  9. # If these requires were at the top of the file the constant would not be
  10. # defined by the time their files were loaded. Since some of them reopen
  11. # ActiveSupport::Deprecation its autoload would be triggered, resulting in
  12. # a circular require warning for active_support/deprecation.rb.
  13. #
  14. # So, we define the constant first, and load dependencies later.
  15. 24 require "active_support/deprecation/instance_delegator"
  16. 24 require "active_support/deprecation/behaviors"
  17. 24 require "active_support/deprecation/reporting"
  18. 24 require "active_support/deprecation/disallowed"
  19. 24 require "active_support/deprecation/constant_accessor"
  20. 24 require "active_support/deprecation/method_wrappers"
  21. 23 require "active_support/deprecation/proxy_wrappers"
  22. 23 require "active_support/core_ext/module/deprecation"
  23. 23 require "concurrent/atomic/thread_local_var"
  24. 23 include Singleton
  25. 23 include InstanceDelegator
  26. 23 include Behavior
  27. 23 include Reporting
  28. 23 include Disallowed
  29. 23 include MethodWrapper
  30. # The version number in which the deprecated behavior will be removed, by default.
  31. 23 attr_accessor :deprecation_horizon
  32. # It accepts two parameters on initialization. The first is a version of library
  33. # and the second is a library name.
  34. #
  35. # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
  36. 23 def initialize(deprecation_horizon = "6.2", gem_name = "Rails")
  37. 23 self.gem_name = gem_name
  38. 23 self.deprecation_horizon = deprecation_horizon
  39. # By default, warnings are not silenced and debugging is off.
  40. 23 self.silenced = false
  41. 23 self.debug = false
  42. 23 @silenced_thread = Concurrent::ThreadLocalVar.new(false)
  43. 23 @explicitly_allowed_warnings = Concurrent::ThreadLocalVar.new(nil)
  44. end
  45. end
  46. end

lib/active_support/deprecation/behaviors.rb

36.11% lines covered

36 relevant lines. 13 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/notifications"
  3. 24 module ActiveSupport
  4. # Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>.
  5. # You would set <tt>:raise</tt>, as a behavior to raise errors and proactively report exceptions from deprecations.
  6. 24 class DeprecationException < StandardError
  7. end
  8. 24 class Deprecation
  9. # Default warning behaviors per Rails.env.
  10. 24 DEFAULT_BEHAVIORS = {
  11. raise: ->(message, callstack, deprecation_horizon, gem_name) {
  12. e = DeprecationException.new(message)
  13. e.set_backtrace(callstack.map(&:to_s))
  14. raise e
  15. },
  16. stderr: ->(message, callstack, deprecation_horizon, gem_name) {
  17. $stderr.puts(message)
  18. $stderr.puts callstack.join("\n ") if debug
  19. },
  20. log: ->(message, callstack, deprecation_horizon, gem_name) {
  21. logger =
  22. if defined?(Rails.logger) && Rails.logger
  23. Rails.logger
  24. else
  25. require "active_support/logger"
  26. ActiveSupport::Logger.new($stderr)
  27. end
  28. logger.warn message
  29. logger.debug callstack.join("\n ") if debug
  30. },
  31. notify: ->(message, callstack, deprecation_horizon, gem_name) {
  32. notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}"
  33. ActiveSupport::Notifications.instrument(notification_name,
  34. message: message,
  35. callstack: callstack,
  36. gem_name: gem_name,
  37. deprecation_horizon: deprecation_horizon)
  38. },
  39. silence: ->(message, callstack, deprecation_horizon, gem_name) { },
  40. }
  41. # Behavior module allows to determine how to display deprecation messages.
  42. # You can create a custom behavior or set any from the +DEFAULT_BEHAVIORS+
  43. # constant. Available behaviors are:
  44. #
  45. # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
  46. # [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
  47. # [+log+] Log all deprecation warnings to +Rails.logger+.
  48. # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
  49. # [+silence+] Do nothing.
  50. #
  51. # Setting behaviors only affects deprecations that happen after boot time.
  52. # For more information you can read the documentation of the +behavior=+ method.
  53. 24 module Behavior
  54. # Whether to print a backtrace along with the warning.
  55. 24 attr_accessor :debug
  56. # Returns the current behavior or if one isn't set, defaults to +:stderr+.
  57. 24 def behavior
  58. @behavior ||= [DEFAULT_BEHAVIORS[:stderr]]
  59. end
  60. # Returns the current behavior for disallowed deprecations or if one isn't set, defaults to +:raise+.
  61. 24 def disallowed_behavior
  62. @disallowed_behavior ||= [DEFAULT_BEHAVIORS[:raise]]
  63. end
  64. # Sets the behavior to the specified value. Can be a single value, array,
  65. # or an object that responds to +call+.
  66. #
  67. # Available behaviors:
  68. #
  69. # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
  70. # [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
  71. # [+log+] Log all deprecation warnings to +Rails.logger+.
  72. # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
  73. # [+silence+] Do nothing.
  74. #
  75. # Setting behaviors only affects deprecations that happen after boot time.
  76. # Deprecation warnings raised by gems are not affected by this setting
  77. # because they happen before Rails boots up.
  78. #
  79. # ActiveSupport::Deprecation.behavior = :stderr
  80. # ActiveSupport::Deprecation.behavior = [:stderr, :log]
  81. # ActiveSupport::Deprecation.behavior = MyCustomHandler
  82. # ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
  83. # # custom stuff
  84. # }
  85. 24 def behavior=(behavior)
  86. @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
  87. end
  88. # Sets the behavior for disallowed deprecations (those configured by
  89. # ActiveSupport::Deprecation.disallowed_warnings=) to the specified
  90. # value. As with +behavior=+, this can be a single value, array, or an
  91. # object that responds to +call+.
  92. 24 def disallowed_behavior=(behavior)
  93. @disallowed_behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
  94. end
  95. 24 private
  96. 24 def arity_coerce(behavior)
  97. unless behavior.respond_to?(:call)
  98. raise ArgumentError, "#{behavior.inspect} is not a valid deprecation behavior."
  99. end
  100. if behavior.arity == 4 || behavior.arity == -1
  101. behavior
  102. else
  103. -> message, callstack, _, _ { behavior.call(message, callstack) }
  104. end
  105. end
  106. end
  107. end
  108. end

lib/active_support/deprecation/constant_accessor.rb

68.75% lines covered

16 relevant lines. 11 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module ActiveSupport
  3. 24 class Deprecation
  4. # DeprecatedConstantAccessor transforms a constant into a deprecated one by
  5. # hooking +const_missing+.
  6. #
  7. # It takes the names of an old (deprecated) constant and of a new constant
  8. # (both in string form) and optionally a deprecator. The deprecator defaults
  9. # to +ActiveSupport::Deprecator+ if none is specified.
  10. #
  11. # The deprecated constant now returns the same object as the new one rather
  12. # than a proxy object, so it can be used transparently in +rescue+ blocks
  13. # etc.
  14. #
  15. # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
  16. #
  17. # # (In a later update, the original implementation of `PLANETS` has been removed.)
  18. #
  19. # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
  20. # include ActiveSupport::Deprecation::DeprecatedConstantAccessor
  21. # deprecate_constant 'PLANETS', 'PLANETS_POST_2006'
  22. #
  23. # PLANETS.map { |planet| planet.capitalize }
  24. # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
  25. # (Backtrace information…)
  26. # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
  27. 24 module DeprecatedConstantAccessor
  28. 24 def self.included(base)
  29. 1 require "active_support/inflector/methods"
  30. 1 extension = Module.new do
  31. 1 def const_missing(missing_const_name)
  32. if class_variable_defined?(:@@_deprecated_constants)
  33. if (replacement = class_variable_get(:@@_deprecated_constants)[missing_const_name.to_s])
  34. replacement[:deprecator].warn(replacement[:message] || "#{name}::#{missing_const_name} is deprecated! Use #{replacement[:new]} instead.", caller_locations)
  35. return ActiveSupport::Inflector.constantize(replacement[:new].to_s)
  36. end
  37. end
  38. super
  39. end
  40. 1 def deprecate_constant(const_name, new_constant, message: nil, deprecator: ActiveSupport::Deprecation.instance)
  41. 2 class_variable_set(:@@_deprecated_constants, {}) unless class_variable_defined?(:@@_deprecated_constants)
  42. 2 class_variable_get(:@@_deprecated_constants)[const_name.to_s] = { new: new_constant, message: message, deprecator: deprecator }
  43. end
  44. end
  45. 1 base.singleton_class.prepend extension
  46. end
  47. end
  48. end
  49. end

lib/active_support/deprecation/disallowed.rb

33.33% lines covered

24 relevant lines. 8 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module ActiveSupport
  3. 24 class Deprecation
  4. 24 module Disallowed
  5. # Sets the criteria used to identify deprecation messages which should be
  6. # disallowed. Can be an array containing strings, symbols, or regular
  7. # expressions. (Symbols are treated as strings). These are compared against
  8. # the text of the generated deprecation warning.
  9. #
  10. # Additionally the scalar symbol +:all+ may be used to treat all
  11. # deprecations as disallowed.
  12. #
  13. # Deprecations matching a substring or regular expression will be handled
  14. # using the configured +ActiveSupport::Deprecation.disallowed_behavior+
  15. # rather than +ActiveSupport::Deprecation.behavior+
  16. 24 attr_writer :disallowed_warnings
  17. # Returns the configured criteria used to identify deprecation messages
  18. # which should be treated as disallowed.
  19. 24 def disallowed_warnings
  20. @disallowed_warnings ||= []
  21. end
  22. 24 private
  23. 24 def deprecation_disallowed?(message)
  24. disallowed = ActiveSupport::Deprecation.disallowed_warnings
  25. return false if explicitly_allowed?(message)
  26. return true if disallowed == :all
  27. disallowed.any? do |rule|
  28. case rule
  29. when String, Symbol
  30. message.include?(rule.to_s)
  31. when Regexp
  32. rule.match?(message)
  33. end
  34. end
  35. end
  36. 24 def explicitly_allowed?(message)
  37. allowances = @explicitly_allowed_warnings.value
  38. return false unless allowances
  39. return true if allowances == :all
  40. allowances = [allowances] unless allowances.kind_of?(Array)
  41. allowances.any? do |rule|
  42. case rule
  43. when String, Symbol
  44. message.include?(rule.to_s)
  45. when Regexp
  46. rule.match?(message)
  47. end
  48. end
  49. end
  50. end
  51. end
  52. end

lib/active_support/deprecation/instance_delegator.rb

80.95% lines covered

21 relevant lines. 17 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/core_ext/module/delegation"
  3. 24 module ActiveSupport
  4. 24 class Deprecation
  5. 24 module InstanceDelegator # :nodoc:
  6. 24 def self.included(base)
  7. 23 base.extend(ClassMethods)
  8. 23 base.singleton_class.prepend(OverrideDelegators)
  9. 23 base.public_class_method :new
  10. end
  11. 24 module ClassMethods # :nodoc:
  12. 24 def include(included_module)
  13. 483 included_module.instance_methods.each { |m| method_added(m) }
  14. 92 super
  15. end
  16. 24 def method_added(method_name)
  17. 460 singleton_class.delegate(method_name, to: :instance)
  18. end
  19. end
  20. 24 module OverrideDelegators # :nodoc:
  21. 24 def warn(message = nil, callstack = nil)
  22. callstack ||= caller_locations(2)
  23. super
  24. end
  25. 24 def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
  26. caller_backtrace ||= caller_locations(2)
  27. super
  28. end
  29. end
  30. end
  31. end
  32. end

lib/active_support/deprecation/method_wrappers.rb

84.62% lines covered

26 relevant lines. 22 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/core_ext/array/extract_options"
  3. 24 require "active_support/core_ext/module/redefine_method"
  4. 23 module ActiveSupport
  5. 23 class Deprecation
  6. 23 module MethodWrapper
  7. # Declare that a method has been deprecated.
  8. #
  9. # class Fred
  10. # def aaa; end
  11. # def bbb; end
  12. # def ccc; end
  13. # def ddd; end
  14. # def eee; end
  15. # end
  16. #
  17. # Using the default deprecator:
  18. # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
  19. # # => Fred
  20. #
  21. # Fred.new.aaa
  22. # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
  23. # # => nil
  24. #
  25. # Fred.new.bbb
  26. # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
  27. # # => nil
  28. #
  29. # Fred.new.ccc
  30. # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
  31. # # => nil
  32. #
  33. # Passing in a custom deprecator:
  34. # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
  35. # ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
  36. # # => [:ddd]
  37. #
  38. # Fred.new.ddd
  39. # DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
  40. # # => nil
  41. #
  42. # Using a custom deprecator directly:
  43. # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
  44. # custom_deprecator.deprecate_methods(Fred, eee: :zzz)
  45. # # => [:eee]
  46. #
  47. # Fred.new.eee
  48. # DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
  49. # # => nil
  50. 23 def deprecate_methods(target_module, *method_names)
  51. 4 options = method_names.extract_options!
  52. 4 deprecator = options.delete(:deprecator) || self
  53. 4 method_names += options.keys
  54. 4 mod = nil
  55. 4 method_names.each do |method_name|
  56. 9 message = options[method_name]
  57. 9 if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
  58. 8 method = target_module.instance_method(method_name)
  59. 8 target_module.module_eval do
  60. 8 redefine_method(method_name) do |*args, &block|
  61. deprecator.deprecation_warning(method_name, message)
  62. method.bind(self).call(*args, &block)
  63. end
  64. 8 ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
  65. end
  66. else
  67. 1 mod ||= Module.new
  68. 1 mod.module_eval do
  69. 1 define_method(method_name) do |*args, &block|
  70. deprecator.deprecation_warning(method_name, message)
  71. super(*args, &block)
  72. end
  73. 1 ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
  74. end
  75. end
  76. end
  77. 4 target_module.prepend(mod) if mod
  78. end
  79. end
  80. end
  81. end

lib/active_support/deprecation/proxy_wrappers.rb

60.66% lines covered

61 relevant lines. 37 lines covered and 24 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport
  3. 23 class Deprecation
  4. 23 class DeprecationProxy #:nodoc:
  5. 23 def self.new(*args, &block)
  6. object = args.first
  7. return object unless object
  8. super
  9. end
  10. 1541 instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
  11. # Don't give a deprecation warning on inspect since test/unit and error
  12. # logs rely on it for diagnostics.
  13. 23 def inspect
  14. target.inspect
  15. end
  16. 23 private
  17. 23 def method_missing(called, *args, &block)
  18. warn caller_locations, called, args
  19. target.__send__(called, *args, &block)
  20. end
  21. end
  22. # DeprecatedObjectProxy transforms an object into a deprecated one. It
  23. # takes an object, a deprecation message and optionally a deprecator. The
  24. # deprecator defaults to +ActiveSupport::Deprecator+ if none is specified.
  25. #
  26. # deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated")
  27. # # => #<Object:0x007fb9b34c34b0>
  28. #
  29. # deprecated_object.to_s
  30. # DEPRECATION WARNING: This object is now deprecated.
  31. # (Backtrace)
  32. # # => "#<Object:0x007fb9b34c34b0>"
  33. 23 class DeprecatedObjectProxy < DeprecationProxy
  34. 23 def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance)
  35. @object = object
  36. @message = message
  37. @deprecator = deprecator
  38. end
  39. 23 private
  40. 23 def target
  41. @object
  42. end
  43. 23 def warn(callstack, called, args)
  44. @deprecator.warn(@message, callstack)
  45. end
  46. end
  47. # DeprecatedInstanceVariableProxy transforms an instance variable into a
  48. # deprecated one. It takes an instance of a class, a method on that class
  49. # and an instance variable. It optionally takes a deprecator as the last
  50. # argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none
  51. # is specified.
  52. #
  53. # class Example
  54. # def initialize
  55. # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request)
  56. # @_request = :special_request
  57. # end
  58. #
  59. # def request
  60. # @_request
  61. # end
  62. #
  63. # def old_request
  64. # @request
  65. # end
  66. # end
  67. #
  68. # example = Example.new
  69. # # => #<Example:0x007fb9b31090b8 @_request=:special_request, @request=:special_request>
  70. #
  71. # example.old_request.to_s
  72. # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of
  73. # @request.to_s
  74. # (Backtrace information…)
  75. # "special_request"
  76. #
  77. # example.request.to_s
  78. # # => "special_request"
  79. 23 class DeprecatedInstanceVariableProxy < DeprecationProxy
  80. 23 def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance)
  81. @instance = instance
  82. @method = method
  83. @var = var
  84. @deprecator = deprecator
  85. end
  86. 23 private
  87. 23 def target
  88. @instance.__send__(@method)
  89. end
  90. 23 def warn(callstack, called, args)
  91. @deprecator.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack)
  92. end
  93. end
  94. # DeprecatedConstantProxy transforms a constant into a deprecated one. It
  95. # takes the names of an old (deprecated) constant and of a new constant
  96. # (both in string form) and optionally a deprecator. The deprecator defaults
  97. # to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant
  98. # now returns the value of the new one.
  99. #
  100. # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
  101. #
  102. # # (In a later update, the original implementation of `PLANETS` has been removed.)
  103. #
  104. # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
  105. # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
  106. #
  107. # PLANETS.map { |planet| planet.capitalize }
  108. # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
  109. # (Backtrace information…)
  110. # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
  111. 23 class DeprecatedConstantProxy < Module
  112. 23 def self.new(*args, **options, &block)
  113. 2 object = args.first
  114. 2 return object unless object
  115. 2 super
  116. end
  117. 23 def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
  118. 2 Kernel.require "active_support/inflector/methods"
  119. 2 @old_const = old_const
  120. 2 @new_const = new_const
  121. 2 @deprecator = deprecator
  122. 2 @message = message
  123. end
  124. 2970 instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
  125. # Don't give a deprecation warning on inspect since test/unit and error
  126. # logs rely on it for diagnostics.
  127. 23 def inspect
  128. target.inspect
  129. end
  130. # Don't give a deprecation warning on methods that IRB may invoke
  131. # during tab-completion.
  132. 23 delegate :hash, :instance_methods, :name, :respond_to?, to: :target
  133. # Returns the class of the new constant.
  134. #
  135. # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
  136. # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
  137. # PLANETS.class # => Array
  138. 23 def class
  139. target.class
  140. end
  141. 23 private
  142. 23 def target
  143. ActiveSupport::Inflector.constantize(@new_const.to_s)
  144. end
  145. 23 def const_missing(name)
  146. @deprecator.warn(@message, caller_locations)
  147. target.const_get(name)
  148. end
  149. 23 def method_missing(called, *args, &block)
  150. @deprecator.warn(@message, caller_locations)
  151. target.__send__(called, *args, &block)
  152. end
  153. end
  154. end
  155. end

lib/active_support/deprecation/reporting.rb

32.76% lines covered

58 relevant lines. 19 lines covered and 39 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "rbconfig"
  3. 24 module ActiveSupport
  4. 24 class Deprecation
  5. 24 module Reporting
  6. # Whether to print a message (silent mode)
  7. 24 attr_writer :silenced
  8. # Name of gem where method is deprecated
  9. 24 attr_accessor :gem_name
  10. # Outputs a deprecation warning to the output configured by
  11. # <tt>ActiveSupport::Deprecation.behavior</tt>.
  12. #
  13. # ActiveSupport::Deprecation.warn('something broke!')
  14. # # => "DEPRECATION WARNING: something broke! (called from your_code.rb:1)"
  15. 24 def warn(message = nil, callstack = nil)
  16. return if silenced
  17. callstack ||= caller_locations(2)
  18. deprecation_message(callstack, message).tap do |m|
  19. if deprecation_disallowed?(message)
  20. disallowed_behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
  21. else
  22. behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
  23. end
  24. end
  25. end
  26. # Silence deprecation warnings within the block.
  27. #
  28. # ActiveSupport::Deprecation.warn('something broke!')
  29. # # => "DEPRECATION WARNING: something broke! (called from your_code.rb:1)"
  30. #
  31. # ActiveSupport::Deprecation.silence do
  32. # ActiveSupport::Deprecation.warn('something broke!')
  33. # end
  34. # # => nil
  35. 24 def silence(&block)
  36. @silenced_thread.bind(true, &block)
  37. end
  38. # Allow previously disallowed deprecation warnings within the block.
  39. # <tt>allowed_warnings</tt> can be an array containing strings, symbols, or regular
  40. # expressions. (Symbols are treated as strings). These are compared against
  41. # the text of deprecation warning messages generated within the block.
  42. # Matching warnings will be exempt from the rules set by
  43. # +ActiveSupport::Deprecation.disallowed_warnings+
  44. #
  45. # The optional <tt>if:</tt> argument accepts a truthy/falsy value or an object that
  46. # responds to <tt>.call</tt>. If truthy, then matching warnings will be allowed.
  47. # If falsey then the method yields to the block without allowing the warning.
  48. #
  49. # ActiveSupport::Deprecation.disallowed_behavior = :raise
  50. # ActiveSupport::Deprecation.disallowed_warnings = [
  51. # "something broke"
  52. # ]
  53. #
  54. # ActiveSupport::Deprecation.warn('something broke!')
  55. # # => ActiveSupport::DeprecationException
  56. #
  57. # ActiveSupport::Deprecation.allow ['something broke'] do
  58. # ActiveSupport::Deprecation.warn('something broke!')
  59. # end
  60. # # => nil
  61. #
  62. # ActiveSupport::Deprecation.allow ['something broke'], if: Rails.env.production? do
  63. # ActiveSupport::Deprecation.warn('something broke!')
  64. # end
  65. # # => ActiveSupport::DeprecationException for dev/test, nil for production
  66. 24 def allow(allowed_warnings = :all, if: true, &block)
  67. conditional = binding.local_variable_get(:if)
  68. conditional = conditional.call if conditional.respond_to?(:call)
  69. if conditional
  70. @explicitly_allowed_warnings.bind(allowed_warnings, &block)
  71. else
  72. yield
  73. end
  74. end
  75. 24 def silenced
  76. @silenced || @silenced_thread.value
  77. end
  78. 24 def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
  79. caller_backtrace ||= caller_locations(2)
  80. deprecated_method_warning(deprecated_method_name, message).tap do |msg|
  81. warn(msg, caller_backtrace)
  82. end
  83. end
  84. 24 private
  85. # Outputs a deprecation warning message
  86. #
  87. # deprecated_method_warning(:method_name)
  88. # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}"
  89. # deprecated_method_warning(:method_name, :another_method)
  90. # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)"
  91. # deprecated_method_warning(:method_name, "Optional message")
  92. # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)"
  93. 24 def deprecated_method_warning(method_name, message = nil)
  94. warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}"
  95. case message
  96. when Symbol then "#{warning} (use #{message} instead)"
  97. when String then "#{warning} (#{message})"
  98. else warning
  99. end
  100. end
  101. 24 def deprecation_message(callstack, message = nil)
  102. message ||= "You are using deprecated behavior which will be removed from the next major or minor release."
  103. "DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}"
  104. end
  105. 24 def deprecation_caller_message(callstack)
  106. file, line, method = extract_callstack(callstack)
  107. if file
  108. if line && method
  109. "(called from #{method} at #{file}:#{line})"
  110. else
  111. "(called from #{file}:#{line})"
  112. end
  113. end
  114. end
  115. 24 def extract_callstack(callstack)
  116. return _extract_callstack(callstack) if callstack.first.is_a? String
  117. offending_line = callstack.find { |frame|
  118. frame.absolute_path && !ignored_callstack(frame.absolute_path)
  119. } || callstack.first
  120. [offending_line.path, offending_line.lineno, offending_line.label]
  121. end
  122. 24 def _extract_callstack(callstack)
  123. warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
  124. offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first
  125. if offending_line
  126. if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
  127. md.captures
  128. else
  129. offending_line
  130. end
  131. end
  132. end
  133. 24 RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
  134. 24 def ignored_callstack(path)
  135. path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
  136. end
  137. end
  138. end
  139. end

lib/active_support/descendants_tracker.rb

58.62% lines covered

58 relevant lines. 34 lines covered and 24 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "weakref"
  3. 23 module ActiveSupport
  4. # This module provides an internal implementation to track descendants
  5. # which is faster than iterating through ObjectSpace.
  6. 23 module DescendantsTracker
  7. 23 @@direct_descendants = {}
  8. 23 class << self
  9. 23 def direct_descendants(klass)
  10. descendants = @@direct_descendants[klass]
  11. descendants ? descendants.to_a : []
  12. end
  13. 23 alias_method :subclasses, :direct_descendants
  14. 23 def descendants(klass)
  15. 211 arr = []
  16. 211 accumulate_descendants(klass, arr)
  17. 211 arr
  18. end
  19. 23 def clear
  20. if defined? ActiveSupport::Dependencies
  21. @@direct_descendants.each do |klass, descendants|
  22. if Dependencies.autoloaded?(klass)
  23. @@direct_descendants.delete(klass)
  24. else
  25. descendants.reject! { |v| Dependencies.autoloaded?(v) }
  26. end
  27. end
  28. else
  29. @@direct_descendants.clear
  30. end
  31. end
  32. # This is the only method that is not thread safe, but is only ever called
  33. # during the eager loading phase.
  34. 23 def store_inherited(klass, descendant)
  35. 334 (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
  36. end
  37. 23 private
  38. 23 def accumulate_descendants(klass, acc)
  39. 211 if direct_descendants = @@direct_descendants[klass]
  40. direct_descendants.each do |direct_descendant|
  41. acc << direct_descendant
  42. accumulate_descendants(direct_descendant, acc)
  43. end
  44. end
  45. end
  46. end
  47. 23 def inherited(base)
  48. 334 DescendantsTracker.store_inherited(self, base)
  49. 334 super
  50. end
  51. 23 def direct_descendants
  52. DescendantsTracker.direct_descendants(self)
  53. end
  54. 23 alias_method :subclasses, :direct_descendants
  55. 23 def descendants
  56. DescendantsTracker.descendants(self)
  57. end
  58. # DescendantsArray is an array that contains weak references to classes.
  59. 23 class DescendantsArray # :nodoc:
  60. 23 include Enumerable
  61. 23 def initialize
  62. 54 @refs = []
  63. end
  64. 23 def initialize_copy(orig)
  65. @refs = @refs.dup
  66. end
  67. 23 def <<(klass)
  68. 334 @refs << WeakRef.new(klass)
  69. end
  70. 23 def each
  71. @refs.reject! do |ref|
  72. yield ref.__getobj__
  73. false
  74. rescue WeakRef::RefError
  75. true
  76. end
  77. self
  78. end
  79. 23 def refs_size
  80. @refs.size
  81. end
  82. 23 def cleanup!
  83. @refs.delete_if { |ref| !ref.weakref_alive? }
  84. end
  85. 23 def reject!
  86. @refs.reject! do |ref|
  87. yield ref.__getobj__
  88. rescue WeakRef::RefError
  89. true
  90. end
  91. end
  92. end
  93. end
  94. end

lib/active_support/digest.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. class Digest #:nodoc:
  4. class <<self
  5. def hash_digest_class
  6. @hash_digest_class ||= ::Digest::MD5
  7. end
  8. def hash_digest_class=(klass)
  9. raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest)
  10. @hash_digest_class = klass
  11. end
  12. def hexdigest(arg)
  13. hash_digest_class.hexdigest(arg)[0...32]
  14. end
  15. end
  16. end
  17. end

lib/active_support/duration.rb

37.62% lines covered

210 relevant lines. 79 lines covered and 131 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/array/conversions"
  3. 23 require "active_support/core_ext/module/delegation"
  4. 23 require "active_support/core_ext/object/acts_like"
  5. 23 require "active_support/core_ext/string/filters"
  6. 23 module ActiveSupport
  7. # Provides accurate date and time measurements using Date#advance and
  8. # Time#advance, respectively. It mainly supports the methods on Numeric.
  9. #
  10. # 1.month.ago # equivalent to Time.now.advance(months: -1)
  11. 23 class Duration
  12. 23 class Scalar < Numeric #:nodoc:
  13. 23 attr_reader :value
  14. 23 delegate :to_i, :to_f, :to_s, to: :value
  15. 23 def initialize(value)
  16. @value = value
  17. end
  18. 23 def coerce(other)
  19. [Scalar.new(other), self]
  20. end
  21. 23 def -@
  22. Scalar.new(-value)
  23. end
  24. 23 def <=>(other)
  25. if Scalar === other || Duration === other
  26. value <=> other.value
  27. elsif Numeric === other
  28. value <=> other
  29. else
  30. nil
  31. end
  32. end
  33. 23 def +(other)
  34. if Duration === other
  35. seconds = value + other.parts.fetch(:seconds, 0)
  36. new_parts = other.parts.merge(seconds: seconds)
  37. new_value = value + other.value
  38. Duration.new(new_value, new_parts)
  39. else
  40. calculate(:+, other)
  41. end
  42. end
  43. 23 def -(other)
  44. if Duration === other
  45. seconds = value - other.parts.fetch(:seconds, 0)
  46. new_parts = other.parts.transform_values(&:-@)
  47. new_parts = new_parts.merge(seconds: seconds)
  48. new_value = value - other.value
  49. Duration.new(new_value, new_parts)
  50. else
  51. calculate(:-, other)
  52. end
  53. end
  54. 23 def *(other)
  55. if Duration === other
  56. new_parts = other.parts.transform_values { |other_value| value * other_value }
  57. new_value = value * other.value
  58. Duration.new(new_value, new_parts)
  59. else
  60. calculate(:*, other)
  61. end
  62. end
  63. 23 def /(other)
  64. if Duration === other
  65. value / other.value
  66. else
  67. calculate(:/, other)
  68. end
  69. end
  70. 23 def %(other)
  71. if Duration === other
  72. Duration.build(value % other.value)
  73. else
  74. calculate(:%, other)
  75. end
  76. end
  77. 23 private
  78. 23 def calculate(op, other)
  79. if Scalar === other
  80. Scalar.new(value.public_send(op, other.value))
  81. elsif Numeric === other
  82. Scalar.new(value.public_send(op, other))
  83. else
  84. raise_type_error(other)
  85. end
  86. end
  87. 23 def raise_type_error(other)
  88. raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
  89. end
  90. end
  91. 23 SECONDS_PER_MINUTE = 60
  92. 23 SECONDS_PER_HOUR = 3600
  93. 23 SECONDS_PER_DAY = 86400
  94. 23 SECONDS_PER_WEEK = 604800
  95. 23 SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year
  96. 23 SECONDS_PER_YEAR = 31556952 # length of a gregorian year (365.2425 days)
  97. 23 PARTS_IN_SECONDS = {
  98. seconds: 1,
  99. minutes: SECONDS_PER_MINUTE,
  100. hours: SECONDS_PER_HOUR,
  101. days: SECONDS_PER_DAY,
  102. weeks: SECONDS_PER_WEEK,
  103. months: SECONDS_PER_MONTH,
  104. years: SECONDS_PER_YEAR
  105. }.freeze
  106. 23 PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze
  107. 23 attr_accessor :value, :parts
  108. 23 autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
  109. 23 autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer"
  110. 23 class << self
  111. # Creates a new Duration from string formatted according to ISO 8601 Duration.
  112. #
  113. # See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
  114. # This method allows negative parts to be present in pattern.
  115. # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
  116. 23 def parse(iso8601duration)
  117. parts = ISO8601Parser.new(iso8601duration).parse!
  118. new(calculate_total_seconds(parts), parts)
  119. end
  120. 23 def ===(other) #:nodoc:
  121. other.is_a?(Duration)
  122. rescue ::NoMethodError
  123. false
  124. end
  125. 23 def seconds(value) #:nodoc:
  126. new(value, seconds: value)
  127. end
  128. 23 def minutes(value) #:nodoc:
  129. new(value * SECONDS_PER_MINUTE, minutes: value)
  130. end
  131. 23 def hours(value) #:nodoc:
  132. new(value * SECONDS_PER_HOUR, hours: value)
  133. end
  134. 23 def days(value) #:nodoc:
  135. new(value * SECONDS_PER_DAY, days: value)
  136. end
  137. 23 def weeks(value) #:nodoc:
  138. new(value * SECONDS_PER_WEEK, weeks: value)
  139. end
  140. 23 def months(value) #:nodoc:
  141. new(value * SECONDS_PER_MONTH, months: value)
  142. end
  143. 23 def years(value) #:nodoc:
  144. new(value * SECONDS_PER_YEAR, years: value)
  145. end
  146. # Creates a new Duration from a seconds value that is converted
  147. # to the individual parts:
  148. #
  149. # ActiveSupport::Duration.build(31556952).parts # => {:years=>1}
  150. # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
  151. #
  152. 23 def build(value)
  153. unless value.is_a?(::Numeric)
  154. raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
  155. end
  156. parts = {}
  157. remainder = value.round(9)
  158. PARTS.each do |part|
  159. unless part == :seconds
  160. part_in_seconds = PARTS_IN_SECONDS[part]
  161. parts[part] = remainder.div(part_in_seconds)
  162. remainder %= part_in_seconds
  163. end
  164. end unless value == 0
  165. parts[:seconds] = remainder
  166. new(value, parts)
  167. end
  168. 23 private
  169. 23 def calculate_total_seconds(parts)
  170. parts.inject(0) do |total, (part, value)|
  171. total + value * PARTS_IN_SECONDS[part]
  172. end
  173. end
  174. end
  175. 23 def initialize(value, parts) #:nodoc:
  176. @value, @parts = value, parts
  177. @parts.reject! { |k, v| v.zero? } unless value == 0
  178. end
  179. 23 def coerce(other) #:nodoc:
  180. case other
  181. when Scalar
  182. [other, self]
  183. when Duration
  184. [Scalar.new(other.value), self]
  185. else
  186. [Scalar.new(other), self]
  187. end
  188. end
  189. # Compares one Duration with another or a Numeric to this Duration.
  190. # Numeric values are treated as seconds.
  191. 23 def <=>(other)
  192. if Duration === other
  193. value <=> other.value
  194. elsif Numeric === other
  195. value <=> other
  196. end
  197. end
  198. # Adds another Duration or a Numeric to this Duration. Numeric values
  199. # are treated as seconds.
  200. 23 def +(other)
  201. if Duration === other
  202. parts = @parts.merge(other.parts) do |_key, value, other_value|
  203. value + other_value
  204. end
  205. Duration.new(value + other.value, parts)
  206. else
  207. seconds = @parts.fetch(:seconds, 0) + other
  208. Duration.new(value + other, @parts.merge(seconds: seconds))
  209. end
  210. end
  211. # Subtracts another Duration or a Numeric from this Duration. Numeric
  212. # values are treated as seconds.
  213. 23 def -(other)
  214. self + (-other)
  215. end
  216. # Multiplies this Duration by a Numeric and returns a new Duration.
  217. 23 def *(other)
  218. if Scalar === other || Duration === other
  219. Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
  220. elsif Numeric === other
  221. Duration.new(value * other, parts.transform_values { |number| number * other })
  222. else
  223. raise_type_error(other)
  224. end
  225. end
  226. # Divides this Duration by a Numeric and returns a new Duration.
  227. 23 def /(other)
  228. if Scalar === other
  229. Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
  230. elsif Duration === other
  231. value / other.value
  232. elsif Numeric === other
  233. Duration.new(value / other, parts.transform_values { |number| number / other })
  234. else
  235. raise_type_error(other)
  236. end
  237. end
  238. # Returns the modulo of this Duration by another Duration or Numeric.
  239. # Numeric values are treated as seconds.
  240. 23 def %(other)
  241. if Duration === other || Scalar === other
  242. Duration.build(value % other.value)
  243. elsif Numeric === other
  244. Duration.build(value % other)
  245. else
  246. raise_type_error(other)
  247. end
  248. end
  249. 23 def -@ #:nodoc:
  250. Duration.new(-value, parts.transform_values(&:-@))
  251. end
  252. 23 def +@ #:nodoc:
  253. self
  254. end
  255. 23 def is_a?(klass) #:nodoc:
  256. Duration == klass || value.is_a?(klass)
  257. end
  258. 23 alias :kind_of? :is_a?
  259. 23 def instance_of?(klass) # :nodoc:
  260. Duration == klass || value.instance_of?(klass)
  261. end
  262. # Returns +true+ if +other+ is also a Duration instance with the
  263. # same +value+, or if <tt>other == value</tt>.
  264. 23 def ==(other)
  265. if Duration === other
  266. other.value == value
  267. else
  268. other == value
  269. end
  270. end
  271. # Returns the amount of seconds a duration covers as a string.
  272. # For more information check to_i method.
  273. #
  274. # 1.day.to_s # => "86400"
  275. 23 def to_s
  276. @value.to_s
  277. end
  278. # Returns the number of seconds that this Duration represents.
  279. #
  280. # 1.minute.to_i # => 60
  281. # 1.hour.to_i # => 3600
  282. # 1.day.to_i # => 86400
  283. #
  284. # Note that this conversion makes some assumptions about the
  285. # duration of some periods, e.g. months are always 1/12 of year
  286. # and years are 365.2425 days:
  287. #
  288. # # equivalent to (1.year / 12).to_i
  289. # 1.month.to_i # => 2629746
  290. #
  291. # # equivalent to 365.2425.days.to_i
  292. # 1.year.to_i # => 31556952
  293. #
  294. # In such cases, Ruby's core
  295. # Date[https://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
  296. # Time[https://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
  297. # date and time arithmetic.
  298. 23 def to_i
  299. @value.to_i
  300. end
  301. # Returns +true+ if +other+ is also a Duration instance, which has the
  302. # same parts as this one.
  303. 23 def eql?(other)
  304. Duration === other && other.value.eql?(value)
  305. end
  306. 23 def hash
  307. @value.hash
  308. end
  309. # Calculates a new Time or Date that is as far in the future
  310. # as this Duration represents.
  311. 23 def since(time = ::Time.current)
  312. sum(1, time)
  313. end
  314. 23 alias :from_now :since
  315. 23 alias :after :since
  316. # Calculates a new Time or Date that is as far in the past
  317. # as this Duration represents.
  318. 23 def ago(time = ::Time.current)
  319. sum(-1, time)
  320. end
  321. 23 alias :until :ago
  322. 23 alias :before :ago
  323. 23 def inspect #:nodoc:
  324. return "#{value} seconds" if parts.empty?
  325. parts.
  326. sort_by { |unit, _ | PARTS.index(unit) }.
  327. map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
  328. to_sentence(locale: ::I18n.default_locale)
  329. end
  330. 23 def as_json(options = nil) #:nodoc:
  331. to_i
  332. end
  333. 23 def init_with(coder) #:nodoc:
  334. initialize(coder["value"], coder["parts"])
  335. end
  336. 23 def encode_with(coder) #:nodoc:
  337. coder.map = { "value" => @value, "parts" => @parts }
  338. end
  339. # Build ISO 8601 Duration string for this duration.
  340. # The +precision+ parameter can be used to limit seconds' precision of duration.
  341. 23 def iso8601(precision: nil)
  342. ISO8601Serializer.new(self, precision: precision).serialize
  343. end
  344. 23 private
  345. 23 def sum(sign, time = ::Time.current)
  346. unless time.acts_like?(:time) || time.acts_like?(:date)
  347. raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
  348. end
  349. if parts.empty?
  350. time.since(sign * value)
  351. else
  352. parts.inject(time) do |t, (type, number)|
  353. if type == :seconds
  354. t.since(sign * number)
  355. elsif type == :minutes
  356. t.since(sign * number * 60)
  357. elsif type == :hours
  358. t.since(sign * number * 3600)
  359. else
  360. t.advance(type => sign * number)
  361. end
  362. end
  363. end
  364. end
  365. 23 def respond_to_missing?(method, _)
  366. value.respond_to?(method)
  367. end
  368. 23 def method_missing(method, *args, &block)
  369. value.public_send(method, *args, &block)
  370. end
  371. 23 def raise_type_error(other)
  372. raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
  373. end
  374. end
  375. end

lib/active_support/duration/iso8601_parser.rb

0.0% lines covered

90 relevant lines. 0 lines covered and 90 lines missed.
    
  1. # frozen_string_literal: true
  2. require "strscan"
  3. module ActiveSupport
  4. class Duration
  5. # Parses a string formatted according to ISO 8601 Duration into the hash.
  6. #
  7. # See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
  8. #
  9. # This parser allows negative parts to be present in pattern.
  10. class ISO8601Parser # :nodoc:
  11. class ParsingError < ::ArgumentError; end
  12. PERIOD_OR_COMMA = /\.|,/
  13. PERIOD = "."
  14. COMMA = ","
  15. SIGN_MARKER = /\A\-|\+|/
  16. DATE_MARKER = /P/
  17. TIME_MARKER = /T/
  18. DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
  19. TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
  20. DATE_TO_PART = { "Y" => :years, "M" => :months, "W" => :weeks, "D" => :days }
  21. TIME_TO_PART = { "H" => :hours, "M" => :minutes, "S" => :seconds }
  22. DATE_COMPONENTS = [:years, :months, :days]
  23. TIME_COMPONENTS = [:hours, :minutes, :seconds]
  24. attr_reader :parts, :scanner
  25. attr_accessor :mode, :sign
  26. def initialize(string)
  27. @scanner = StringScanner.new(string)
  28. @parts = {}
  29. @mode = :start
  30. @sign = 1
  31. end
  32. def parse!
  33. while !finished?
  34. case mode
  35. when :start
  36. if scan(SIGN_MARKER)
  37. self.sign = (scanner.matched == "-") ? -1 : 1
  38. self.mode = :sign
  39. else
  40. raise_parsing_error
  41. end
  42. when :sign
  43. if scan(DATE_MARKER)
  44. self.mode = :date
  45. else
  46. raise_parsing_error
  47. end
  48. when :date
  49. if scan(TIME_MARKER)
  50. self.mode = :time
  51. elsif scan(DATE_COMPONENT)
  52. parts[DATE_TO_PART[scanner[2]]] = number * sign
  53. else
  54. raise_parsing_error
  55. end
  56. when :time
  57. if scan(TIME_COMPONENT)
  58. parts[TIME_TO_PART[scanner[2]]] = number * sign
  59. else
  60. raise_parsing_error
  61. end
  62. end
  63. end
  64. validate!
  65. parts
  66. end
  67. private
  68. def finished?
  69. scanner.eos?
  70. end
  71. # Parses number which can be a float with either comma or period.
  72. def number
  73. PERIOD_OR_COMMA.match?(scanner[1]) ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
  74. end
  75. def scan(pattern)
  76. scanner.scan(pattern)
  77. end
  78. def raise_parsing_error(reason = nil)
  79. raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
  80. end
  81. # Checks for various semantic errors as stated in ISO 8601 standard.
  82. def validate!
  83. raise_parsing_error("is empty duration") if parts.empty?
  84. # Mixing any of Y, M, D with W is invalid.
  85. if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
  86. raise_parsing_error("mixing weeks with other date parts not allowed")
  87. end
  88. # Specifying an empty T part is invalid.
  89. if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
  90. raise_parsing_error("time part marker is present but time part is empty")
  91. end
  92. fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
  93. unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
  94. raise_parsing_error "(only last part can be fractional)"
  95. end
  96. true
  97. end
  98. end
  99. end
  100. end

lib/active_support/duration/iso8601_serializer.rb

0.0% lines covered

47 relevant lines. 0 lines covered and 47 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/object/blank"
  3. module ActiveSupport
  4. class Duration
  5. # Serializes duration to string according to ISO 8601 Duration format.
  6. class ISO8601Serializer # :nodoc:
  7. DATE_COMPONENTS = %i(years months days)
  8. def initialize(duration, precision: nil)
  9. @duration = duration
  10. @precision = precision
  11. end
  12. # Builds and returns output string.
  13. def serialize
  14. parts, sign = normalize
  15. return "PT0S" if parts.empty?
  16. output = +"P"
  17. output << "#{parts[:years]}Y" if parts.key?(:years)
  18. output << "#{parts[:months]}M" if parts.key?(:months)
  19. output << "#{parts[:days]}D" if parts.key?(:days)
  20. output << "#{parts[:weeks]}W" if parts.key?(:weeks)
  21. time = +""
  22. time << "#{parts[:hours]}H" if parts.key?(:hours)
  23. time << "#{parts[:minutes]}M" if parts.key?(:minutes)
  24. if parts.key?(:seconds)
  25. time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
  26. end
  27. output << "T#{time}" unless time.empty?
  28. "#{sign}#{output}"
  29. end
  30. private
  31. # Return pair of duration's parts and whole duration sign.
  32. # Parts are summarized (as they can become repetitive due to addition, etc).
  33. # Zero parts are removed as not significant.
  34. # If all parts are negative it will negate all of them and return minus as a sign.
  35. def normalize
  36. parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
  37. p[k] += v unless v.zero?
  38. end
  39. # Convert weeks to days and remove weeks if mixed with date parts
  40. if week_mixed_with_date?(parts)
  41. parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
  42. end
  43. # If all parts are negative - let's make a negative duration
  44. sign = ""
  45. if parts.values.all? { |v| v < 0 }
  46. sign = "-"
  47. parts.transform_values!(&:-@)
  48. end
  49. [parts, sign]
  50. end
  51. def week_mixed_with_date?(parts)
  52. parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
  53. end
  54. end
  55. end
  56. end

lib/active_support/encrypted_configuration.rb

66.67% lines covered

24 relevant lines. 16 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "yaml"
  3. 2 require "active_support/encrypted_file"
  4. 2 require "active_support/ordered_options"
  5. 2 require "active_support/core_ext/object/inclusion"
  6. 2 require "active_support/core_ext/module/delegation"
  7. 2 module ActiveSupport
  8. 2 class EncryptedConfiguration < EncryptedFile
  9. 2 delegate :[], :fetch, to: :config
  10. 2 delegate_missing_to :options
  11. 2 def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
  12. super content_path: config_path, key_path: key_path,
  13. env_key: env_key, raise_if_missing_key: raise_if_missing_key
  14. end
  15. # Allow a config to be started without a file present
  16. 2 def read
  17. super
  18. rescue ActiveSupport::EncryptedFile::MissingContentError
  19. ""
  20. end
  21. 2 def write(contents)
  22. deserialize(contents)
  23. super
  24. end
  25. 2 def config
  26. @config ||= deserialize(read).deep_symbolize_keys
  27. end
  28. 2 private
  29. 2 def options
  30. @options ||= ActiveSupport::InheritableOptions.new(config)
  31. end
  32. 2 def deserialize(config)
  33. YAML.load(config).presence || {}
  34. end
  35. end
  36. end

lib/active_support/encrypted_file.rb

49.02% lines covered

51 relevant lines. 25 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "pathname"
  3. 2 require "tmpdir"
  4. 2 require "active_support/message_encryptor"
  5. 2 module ActiveSupport
  6. 2 class EncryptedFile
  7. 2 class MissingContentError < RuntimeError
  8. 2 def initialize(content_path)
  9. super "Missing encrypted content file in #{content_path}."
  10. end
  11. end
  12. 2 class MissingKeyError < RuntimeError
  13. 2 def initialize(key_path:, env_key:)
  14. super \
  15. "Missing encryption key to decrypt file with. " +
  16. "Ask your team for your master key and write it to #{key_path} or put it in the ENV['#{env_key}']."
  17. end
  18. end
  19. 2 CIPHER = "aes-128-gcm"
  20. 2 def self.generate_key
  21. SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
  22. end
  23. 2 attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
  24. 2 def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
  25. @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path }
  26. @key_path = Pathname.new(key_path)
  27. @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
  28. end
  29. 2 def key
  30. read_env_key || read_key_file || handle_missing_key
  31. end
  32. 2 def read
  33. if !key.nil? && content_path.exist?
  34. decrypt content_path.binread
  35. else
  36. raise MissingContentError, content_path
  37. end
  38. end
  39. 2 def write(contents)
  40. IO.binwrite "#{content_path}.tmp", encrypt(contents)
  41. FileUtils.mv "#{content_path}.tmp", content_path
  42. end
  43. 2 def change(&block)
  44. writing read, &block
  45. end
  46. 2 private
  47. 2 def writing(contents)
  48. tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
  49. tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
  50. tmp_path.binwrite contents
  51. yield tmp_path
  52. updated_contents = tmp_path.binread
  53. write(updated_contents) if updated_contents != contents
  54. ensure
  55. FileUtils.rm(tmp_path) if tmp_path&.exist?
  56. end
  57. 2 def encrypt(contents)
  58. encryptor.encrypt_and_sign contents
  59. end
  60. 2 def decrypt(contents)
  61. encryptor.decrypt_and_verify contents
  62. end
  63. 2 def encryptor
  64. @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
  65. end
  66. 2 def read_env_key
  67. ENV[env_key]
  68. end
  69. 2 def read_key_file
  70. key_path.binread.strip if key_path.exist?
  71. end
  72. 2 def handle_missing_key
  73. raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
  74. end
  75. end
  76. end

lib/active_support/environment_inquirer.rb

70.0% lines covered

10 relevant lines. 7 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/string_inquirer"
  3. 1 module ActiveSupport
  4. 1 class EnvironmentInquirer < StringInquirer #:nodoc:
  5. 1 DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
  6. 1 def initialize(env)
  7. super(env)
  8. DEFAULT_ENVIRONMENTS.each do |default|
  9. instance_variable_set :"@#{default}", env == default
  10. end
  11. end
  12. 1 DEFAULT_ENVIRONMENTS.each do |env|
  13. 3 class_eval "def #{env}?; @#{env}; end"
  14. end
  15. end
  16. end

lib/active_support/evented_file_update_checker.rb

0.0% lines covered

143 relevant lines. 0 lines covered and 143 lines missed.
    
  1. # frozen_string_literal: true
  2. require "set"
  3. require "pathname"
  4. require "concurrent/atomic/atomic_boolean"
  5. require "listen"
  6. module ActiveSupport
  7. # Allows you to "listen" to changes in a file system.
  8. # The evented file updater does not hit disk when checking for updates
  9. # instead it uses platform specific file system events to trigger a change
  10. # in state.
  11. #
  12. # The file checker takes an array of files to watch or a hash specifying directories
  13. # and file extensions to watch. It also takes a block that is called when
  14. # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
  15. # is run and there have been changes to the file system.
  16. #
  17. # Note: Forking will cause the first call to `updated?` to return `true`.
  18. #
  19. # Example:
  20. #
  21. # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
  22. # checker.updated?
  23. # # => false
  24. # checker.execute_if_updated
  25. # # => nil
  26. #
  27. # FileUtils.touch("/tmp/foo")
  28. #
  29. # checker.updated?
  30. # # => true
  31. # checker.execute_if_updated
  32. # # => "changed"
  33. #
  34. class EventedFileUpdateChecker #:nodoc: all
  35. def initialize(files, dirs = {}, &block)
  36. unless block
  37. raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
  38. end
  39. @ph = PathHelper.new
  40. @files = files.map { |f| @ph.xpath(f) }.to_set
  41. @dirs = {}
  42. dirs.each do |dir, exts|
  43. @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
  44. end
  45. @block = block
  46. @updated = Concurrent::AtomicBoolean.new(false)
  47. @lcsp = @ph.longest_common_subpath(@dirs.keys)
  48. @pid = Process.pid
  49. @boot_mutex = Mutex.new
  50. dtw = directories_to_watch
  51. @dtw, @missing = dtw.partition(&:exist?)
  52. boot!
  53. end
  54. def updated?
  55. @boot_mutex.synchronize do
  56. if @pid != Process.pid
  57. boot!
  58. @pid = Process.pid
  59. @updated.make_true
  60. end
  61. end
  62. if @missing.any?(&:exist?)
  63. @boot_mutex.synchronize do
  64. appeared, @missing = @missing.partition(&:exist?)
  65. shutdown!
  66. @dtw += appeared
  67. boot!
  68. @updated.make_true
  69. end
  70. end
  71. @updated.true?
  72. end
  73. def execute
  74. @updated.make_false
  75. @block.call
  76. end
  77. def execute_if_updated
  78. if updated?
  79. yield if block_given?
  80. execute
  81. true
  82. end
  83. end
  84. private
  85. def boot!
  86. normalize_dirs!
  87. Listen.to(*@dtw, &method(:changed)).start if @dtw.any?
  88. end
  89. def shutdown!
  90. Listen.stop
  91. end
  92. def normalize_dirs!
  93. @dirs.transform_keys! do |dir|
  94. dir.exist? ? dir.realpath : dir
  95. end
  96. end
  97. def changed(modified, added, removed)
  98. unless updated?
  99. @updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
  100. end
  101. end
  102. def watching?(file)
  103. file = @ph.xpath(file)
  104. if @files.member?(file)
  105. true
  106. elsif file.directory?
  107. false
  108. else
  109. ext = @ph.normalize_extension(file.extname)
  110. file.dirname.ascend do |dir|
  111. matching = @dirs[dir]
  112. if matching && (matching.empty? || matching.include?(ext))
  113. break true
  114. elsif dir == @lcsp || dir.root?
  115. break false
  116. end
  117. end
  118. end
  119. end
  120. def directories_to_watch
  121. dtw = @files.map(&:dirname) + @dirs.keys
  122. dtw.compact!
  123. dtw.uniq!
  124. normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
  125. dtw = dtw.reject do |path|
  126. normalized_gem_paths.any? { |gem_path| path.to_path.start_with?(gem_path) }
  127. end
  128. @ph.filter_out_descendants(dtw)
  129. end
  130. class PathHelper
  131. def xpath(path)
  132. Pathname.new(path).expand_path
  133. end
  134. def normalize_extension(ext)
  135. ext.to_s.delete_prefix(".")
  136. end
  137. # Given a collection of Pathname objects returns the longest subpath
  138. # common to all of them, or +nil+ if there is none.
  139. def longest_common_subpath(paths)
  140. return if paths.empty?
  141. lcsp = Pathname.new(paths[0])
  142. paths[1..-1].each do |path|
  143. until ascendant_of?(lcsp, path)
  144. if lcsp.root?
  145. # If we get here a root directory is not an ascendant of path.
  146. # This may happen if there are paths in different drives on
  147. # Windows.
  148. return
  149. else
  150. lcsp = lcsp.parent
  151. end
  152. end
  153. end
  154. lcsp
  155. end
  156. # Filters out directories which are descendants of others in the collection (stable).
  157. def filter_out_descendants(dirs)
  158. return dirs if dirs.length < 2
  159. dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
  160. descendants = []
  161. until dirs_sorted_by_nparts.empty?
  162. dir = dirs_sorted_by_nparts.shift
  163. dirs_sorted_by_nparts.reject! do |possible_descendant|
  164. ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
  165. end
  166. end
  167. # Array#- preserves order.
  168. dirs - descendants
  169. end
  170. private
  171. def ascendant_of?(base, other)
  172. base != other && other.ascend do |ascendant|
  173. break true if base == ascendant
  174. end
  175. end
  176. end
  177. end
  178. end

lib/active_support/execution_wrapper.rb

0.0% lines covered

90 relevant lines. 0 lines covered and 90 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/callbacks"
  3. require "concurrent/hash"
  4. module ActiveSupport
  5. class ExecutionWrapper
  6. include ActiveSupport::Callbacks
  7. Null = Object.new # :nodoc:
  8. def Null.complete! # :nodoc:
  9. end
  10. define_callbacks :run
  11. define_callbacks :complete
  12. def self.to_run(*args, &block)
  13. set_callback(:run, *args, &block)
  14. end
  15. def self.to_complete(*args, &block)
  16. set_callback(:complete, *args, &block)
  17. end
  18. RunHook = Struct.new(:hook) do # :nodoc:
  19. def before(target)
  20. hook_state = target.send(:hook_state)
  21. hook_state[hook] = hook.run
  22. end
  23. end
  24. CompleteHook = Struct.new(:hook) do # :nodoc:
  25. def before(target)
  26. hook_state = target.send(:hook_state)
  27. if hook_state.key?(hook)
  28. hook.complete hook_state[hook]
  29. end
  30. end
  31. alias after before
  32. end
  33. # Register an object to be invoked during both the +run+ and
  34. # +complete+ steps.
  35. #
  36. # +hook.complete+ will be passed the value returned from +hook.run+,
  37. # and will only be invoked if +run+ has previously been called.
  38. # (Mostly, this means it won't be invoked if an exception occurs in
  39. # a preceding +to_run+ block; all ordinary +to_complete+ blocks are
  40. # invoked in that situation.)
  41. def self.register_hook(hook, outer: false)
  42. if outer
  43. to_run RunHook.new(hook), prepend: true
  44. to_complete :after, CompleteHook.new(hook)
  45. else
  46. to_run RunHook.new(hook)
  47. to_complete CompleteHook.new(hook)
  48. end
  49. end
  50. # Run this execution.
  51. #
  52. # Returns an instance, whose +complete!+ method *must* be invoked
  53. # after the work has been performed.
  54. #
  55. # Where possible, prefer +wrap+.
  56. def self.run!
  57. if active?
  58. Null
  59. else
  60. new.tap do |instance|
  61. success = nil
  62. begin
  63. instance.run!
  64. success = true
  65. ensure
  66. instance.complete! unless success
  67. end
  68. end
  69. end
  70. end
  71. # Perform the work in the supplied block as an execution.
  72. def self.wrap
  73. return yield if active?
  74. instance = run!
  75. begin
  76. yield
  77. ensure
  78. instance.complete!
  79. end
  80. end
  81. class << self # :nodoc:
  82. attr_accessor :active
  83. end
  84. def self.inherited(other) # :nodoc:
  85. super
  86. other.active = Concurrent::Hash.new
  87. end
  88. self.active = Concurrent::Hash.new
  89. def self.active? # :nodoc:
  90. @active[Thread.current]
  91. end
  92. def run! # :nodoc:
  93. self.class.active[Thread.current] = true
  94. run_callbacks(:run)
  95. end
  96. # Complete this in-flight execution. This method *must* be called
  97. # exactly once on the result of any call to +run!+.
  98. #
  99. # Where possible, prefer +wrap+.
  100. def complete!
  101. run_callbacks(:complete)
  102. ensure
  103. self.class.active.delete Thread.current
  104. end
  105. private
  106. def hook_state
  107. @_hook_state ||= {}
  108. end
  109. end
  110. end

lib/active_support/executor.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/execution_wrapper"
  3. module ActiveSupport
  4. class Executor < ExecutionWrapper
  5. end
  6. end

lib/active_support/file_update_checker.rb

0.0% lines covered

89 relevant lines. 0 lines covered and 89 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/time/calculations"
  3. module ActiveSupport
  4. # FileUpdateChecker specifies the API used by Rails to watch files
  5. # and control reloading. The API depends on four methods:
  6. #
  7. # * +initialize+ which expects two parameters and one block as
  8. # described below.
  9. #
  10. # * +updated?+ which returns a boolean if there were updates in
  11. # the filesystem or not.
  12. #
  13. # * +execute+ which executes the given block on initialization
  14. # and updates the latest watched files and timestamp.
  15. #
  16. # * +execute_if_updated+ which just executes the block if it was updated.
  17. #
  18. # After initialization, a call to +execute_if_updated+ must execute
  19. # the block only if there was really a change in the filesystem.
  20. #
  21. # This class is used by Rails to reload the I18n framework whenever
  22. # they are changed upon a new request.
  23. #
  24. # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
  25. # I18n.reload!
  26. # end
  27. #
  28. # ActiveSupport::Reloader.to_prepare do
  29. # i18n_reloader.execute_if_updated
  30. # end
  31. class FileUpdateChecker
  32. # It accepts two parameters on initialization. The first is an array
  33. # of files and the second is an optional hash of directories. The hash must
  34. # have directories as keys and the value is an array of extensions to be
  35. # watched under that directory.
  36. #
  37. # This method must also receive a block that will be called once a path
  38. # changes. The array of files and list of directories cannot be changed
  39. # after FileUpdateChecker has been initialized.
  40. def initialize(files, dirs = {}, &block)
  41. unless block
  42. raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
  43. end
  44. @files = files.freeze
  45. @glob = compile_glob(dirs)
  46. @block = block
  47. @watched = nil
  48. @updated_at = nil
  49. @last_watched = watched
  50. @last_update_at = updated_at(@last_watched)
  51. end
  52. # Check if any of the entries were updated. If so, the watched and/or
  53. # updated_at values are cached until the block is executed via +execute+
  54. # or +execute_if_updated+.
  55. def updated?
  56. current_watched = watched
  57. if @last_watched.size != current_watched.size
  58. @watched = current_watched
  59. true
  60. else
  61. current_updated_at = updated_at(current_watched)
  62. if @last_update_at < current_updated_at
  63. @watched = current_watched
  64. @updated_at = current_updated_at
  65. true
  66. else
  67. false
  68. end
  69. end
  70. end
  71. # Executes the given block and updates the latest watched files and
  72. # timestamp.
  73. def execute
  74. @last_watched = watched
  75. @last_update_at = updated_at(@last_watched)
  76. @block.call
  77. ensure
  78. @watched = nil
  79. @updated_at = nil
  80. end
  81. # Execute the block given if updated.
  82. def execute_if_updated
  83. if updated?
  84. yield if block_given?
  85. execute
  86. true
  87. else
  88. false
  89. end
  90. end
  91. private
  92. def watched
  93. @watched || begin
  94. all = @files.select { |f| File.exist?(f) }
  95. all.concat(Dir[@glob]) if @glob
  96. all
  97. end
  98. end
  99. def updated_at(paths)
  100. @updated_at || max_mtime(paths) || Time.at(0)
  101. end
  102. # This method returns the maximum mtime of the files in +paths+, or +nil+
  103. # if the array is empty.
  104. #
  105. # Files with a mtime in the future are ignored. Such abnormal situation
  106. # can happen for example if the user changes the clock by hand. It is
  107. # healthy to consider this edge case because with mtimes in the future
  108. # reloading is not triggered.
  109. def max_mtime(paths)
  110. time_now = Time.now
  111. max_mtime = nil
  112. # Time comparisons are performed with #compare_without_coercion because
  113. # AS redefines these operators in a way that is much slower and does not
  114. # bring any benefit in this particular code.
  115. #
  116. # Read t1.compare_without_coercion(t2) < 0 as t1 < t2.
  117. paths.each do |path|
  118. mtime = File.mtime(path)
  119. next if time_now.compare_without_coercion(mtime) < 0
  120. if max_mtime.nil? || max_mtime.compare_without_coercion(mtime) < 0
  121. max_mtime = mtime
  122. end
  123. end
  124. max_mtime
  125. end
  126. def compile_glob(hash)
  127. hash.freeze # Freeze so changes aren't accidentally pushed
  128. return if hash.empty?
  129. globs = hash.map do |key, value|
  130. "#{escape(key)}/**/*#{compile_ext(value)}"
  131. end
  132. "{#{globs.join(",")}}"
  133. end
  134. def escape(key)
  135. key.gsub(",", '\,')
  136. end
  137. def compile_ext(array)
  138. array = Array(array)
  139. return if array.empty?
  140. ".{#{array.join(",")}}"
  141. end
  142. end
  143. end

lib/active_support/fork_tracker.rb

0.0% lines covered

49 relevant lines. 0 lines covered and 49 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. module ForkTracker # :nodoc:
  4. module CoreExt
  5. def fork(*)
  6. if block_given?
  7. super do
  8. ForkTracker.check!
  9. yield
  10. end
  11. else
  12. unless pid = super
  13. ForkTracker.check!
  14. end
  15. pid
  16. end
  17. end
  18. end
  19. module CoreExtPrivate
  20. include CoreExt
  21. private :fork
  22. end
  23. @pid = Process.pid
  24. @callbacks = []
  25. class << self
  26. def check!
  27. if @pid != Process.pid
  28. @callbacks.each(&:call)
  29. @pid = Process.pid
  30. end
  31. end
  32. def hook!
  33. if Process.respond_to?(:fork)
  34. ::Object.prepend(CoreExtPrivate)
  35. ::Kernel.prepend(CoreExtPrivate)
  36. ::Kernel.singleton_class.prepend(CoreExt)
  37. ::Process.singleton_class.prepend(CoreExt)
  38. end
  39. end
  40. def after_fork(&block)
  41. @callbacks << block
  42. block
  43. end
  44. def unregister(callback)
  45. @callbacks.delete(callback)
  46. end
  47. end
  48. end
  49. end
  50. ActiveSupport::ForkTracker.hook!

lib/active_support/gem_version.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module ActiveSupport
  3. # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>.
  4. 24 def self.gem_version
  5. Gem::Version.new VERSION::STRING
  6. end
  7. 24 module VERSION
  8. 24 MAJOR = 6
  9. 24 MINOR = 1
  10. 24 TINY = 0
  11. 24 PRE = "alpha"
  12. 24 STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
  13. end
  14. end

lib/active_support/gzip.rb

0.0% lines covered

23 relevant lines. 0 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. require "zlib"
  3. require "stringio"
  4. module ActiveSupport
  5. # A convenient wrapper for the zlib standard library that allows
  6. # compression/decompression of strings with gzip.
  7. #
  8. # gzip = ActiveSupport::Gzip.compress('compress me!')
  9. # # => "\x1F\x8B\b\x00o\x8D\xCDO\x00\x03K\xCE\xCF-(J-.V\xC8MU\x04\x00R>n\x83\f\x00\x00\x00"
  10. #
  11. # ActiveSupport::Gzip.decompress(gzip)
  12. # # => "compress me!"
  13. module Gzip
  14. class Stream < StringIO
  15. def initialize(*)
  16. super
  17. set_encoding "BINARY"
  18. end
  19. def close; rewind; end
  20. end
  21. # Decompresses a gzipped string.
  22. def self.decompress(source)
  23. Zlib::GzipReader.wrap(StringIO.new(source), &:read)
  24. end
  25. # Compresses a string using gzip.
  26. def self.compress(source, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY)
  27. output = Stream.new
  28. gz = Zlib::GzipWriter.new(output, level, strategy)
  29. gz.write(source)
  30. gz.close
  31. output.string
  32. end
  33. end
  34. end

lib/active_support/hash_with_indifferent_access.rb

45.26% lines covered

137 relevant lines. 62 lines covered and 75 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/keys"
  3. 1 require "active_support/core_ext/hash/reverse_merge"
  4. 1 require "active_support/core_ext/hash/except"
  5. 1 module ActiveSupport
  6. # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
  7. # to be the same.
  8. #
  9. # rgb = ActiveSupport::HashWithIndifferentAccess.new
  10. #
  11. # rgb[:black] = '#000000'
  12. # rgb[:black] # => '#000000'
  13. # rgb['black'] # => '#000000'
  14. #
  15. # rgb['white'] = '#FFFFFF'
  16. # rgb[:white] # => '#FFFFFF'
  17. # rgb['white'] # => '#FFFFFF'
  18. #
  19. # Internally symbols are mapped to strings when used as keys in the entire
  20. # writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This
  21. # mapping belongs to the public interface. For example, given:
  22. #
  23. # hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
  24. #
  25. # You are guaranteed that the key is returned as a string:
  26. #
  27. # hash.keys # => ["a"]
  28. #
  29. # Technically other types of keys are accepted:
  30. #
  31. # hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
  32. # hash[0] = 0
  33. # hash # => {"a"=>1, 0=>0}
  34. #
  35. # but this class is intended for use cases where strings or symbols are the
  36. # expected keys and it is convenient to understand both as the same. For
  37. # example the +params+ hash in Ruby on Rails.
  38. #
  39. # Note that core extensions define <tt>Hash#with_indifferent_access</tt>:
  40. #
  41. # rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
  42. #
  43. # which may be handy.
  44. #
  45. # To access this class outside of Rails, require the core extension with:
  46. #
  47. # require "active_support/core_ext/hash/indifferent_access"
  48. #
  49. # which will, in turn, require this file.
  50. 1 class HashWithIndifferentAccess < Hash
  51. # Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
  52. # this class.
  53. 1 def extractable_options?
  54. true
  55. end
  56. 1 def with_indifferent_access
  57. dup
  58. end
  59. 1 def nested_under_indifferent_access
  60. self
  61. end
  62. 1 def initialize(constructor = {})
  63. if constructor.respond_to?(:to_hash)
  64. super()
  65. update(constructor)
  66. hash = constructor.is_a?(Hash) ? constructor : constructor.to_hash
  67. self.default = hash.default if hash.default
  68. self.default_proc = hash.default_proc if hash.default_proc
  69. else
  70. super(constructor)
  71. end
  72. end
  73. 1 def self.[](*args)
  74. new.merge!(Hash[*args])
  75. end
  76. 1 alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
  77. 1 alias_method :regular_update, :update unless method_defined?(:regular_update)
  78. # Assigns a new value to the hash:
  79. #
  80. # hash = ActiveSupport::HashWithIndifferentAccess.new
  81. # hash[:key] = 'value'
  82. #
  83. # This value can be later fetched using either +:key+ or <tt>'key'</tt>.
  84. 1 def []=(key, value)
  85. regular_writer(convert_key(key), convert_value(value, conversion: :assignment))
  86. end
  87. 1 alias_method :store, :[]=
  88. # Updates the receiver in-place, merging in the hashes passed as arguments:
  89. #
  90. # hash_1 = ActiveSupport::HashWithIndifferentAccess.new
  91. # hash_1[:key] = 'value'
  92. #
  93. # hash_2 = ActiveSupport::HashWithIndifferentAccess.new
  94. # hash_2[:key] = 'New Value!'
  95. #
  96. # hash_1.update(hash_2) # => {"key"=>"New Value!"}
  97. #
  98. # hash = ActiveSupport::HashWithIndifferentAccess.new
  99. # hash.update({ "a" => 1 }, { "b" => 2 }) # => { "a" => 1, "b" => 2 }
  100. #
  101. # The arguments can be either an
  102. # <tt>ActiveSupport::HashWithIndifferentAccess</tt> or a regular +Hash+.
  103. # In either case the merge respects the semantics of indifferent access.
  104. #
  105. # If the argument is a regular hash with keys +:key+ and +"key"+ only one
  106. # of the values end up in the receiver, but which one is unspecified.
  107. #
  108. # When given a block, the value for duplicated keys will be determined
  109. # by the result of invoking the block with the duplicated key, the value
  110. # in the receiver, and the value in +other_hash+. The rules for duplicated
  111. # keys follow the semantics of indifferent access:
  112. #
  113. # hash_1[:key] = 10
  114. # hash_2['key'] = 12
  115. # hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
  116. 1 def update(*other_hashes, &block)
  117. if other_hashes.size == 1
  118. update_with_single_argument(other_hashes.first, block)
  119. else
  120. other_hashes.each do |other_hash|
  121. update_with_single_argument(other_hash, block)
  122. end
  123. end
  124. self
  125. end
  126. 1 alias_method :merge!, :update
  127. # Checks the hash for a key matching the argument passed in:
  128. #
  129. # hash = ActiveSupport::HashWithIndifferentAccess.new
  130. # hash['key'] = 'value'
  131. # hash.key?(:key) # => true
  132. # hash.key?('key') # => true
  133. 1 def key?(key)
  134. super(convert_key(key))
  135. end
  136. 1 alias_method :include?, :key?
  137. 1 alias_method :has_key?, :key?
  138. 1 alias_method :member?, :key?
  139. # Same as <tt>Hash#[]</tt> where the key passed as argument can be
  140. # either a string or a symbol:
  141. #
  142. # counters = ActiveSupport::HashWithIndifferentAccess.new
  143. # counters[:foo] = 1
  144. #
  145. # counters['foo'] # => 1
  146. # counters[:foo] # => 1
  147. # counters[:zoo] # => nil
  148. 1 def [](key)
  149. super(convert_key(key))
  150. end
  151. # Same as <tt>Hash#assoc</tt> where the key passed as argument can be
  152. # either a string or a symbol:
  153. #
  154. # counters = ActiveSupport::HashWithIndifferentAccess.new
  155. # counters[:foo] = 1
  156. #
  157. # counters.assoc('foo') # => ["foo", 1]
  158. # counters.assoc(:foo) # => ["foo", 1]
  159. # counters.assoc(:zoo) # => nil
  160. 1 def assoc(key)
  161. super(convert_key(key))
  162. end
  163. # Same as <tt>Hash#fetch</tt> where the key passed as argument can be
  164. # either a string or a symbol:
  165. #
  166. # counters = ActiveSupport::HashWithIndifferentAccess.new
  167. # counters[:foo] = 1
  168. #
  169. # counters.fetch('foo') # => 1
  170. # counters.fetch(:bar, 0) # => 0
  171. # counters.fetch(:bar) { |key| 0 } # => 0
  172. # counters.fetch(:zoo) # => KeyError: key not found: "zoo"
  173. 1 def fetch(key, *extras)
  174. super(convert_key(key), *extras)
  175. end
  176. # Same as <tt>Hash#dig</tt> where the key passed as argument can be
  177. # either a string or a symbol:
  178. #
  179. # counters = ActiveSupport::HashWithIndifferentAccess.new
  180. # counters[:foo] = { bar: 1 }
  181. #
  182. # counters.dig('foo', 'bar') # => 1
  183. # counters.dig(:foo, :bar) # => 1
  184. # counters.dig(:zoo) # => nil
  185. 1 def dig(*args)
  186. args[0] = convert_key(args[0]) if args.size > 0
  187. super(*args)
  188. end
  189. # Same as <tt>Hash#default</tt> where the key passed as argument can be
  190. # either a string or a symbol:
  191. #
  192. # hash = ActiveSupport::HashWithIndifferentAccess.new(1)
  193. # hash.default # => 1
  194. #
  195. # hash = ActiveSupport::HashWithIndifferentAccess.new { |hash, key| key }
  196. # hash.default # => nil
  197. # hash.default('foo') # => 'foo'
  198. # hash.default(:foo) # => 'foo'
  199. 1 def default(*args)
  200. super(*args.map { |arg| convert_key(arg) })
  201. end
  202. # Returns an array of the values at the specified indices:
  203. #
  204. # hash = ActiveSupport::HashWithIndifferentAccess.new
  205. # hash[:a] = 'x'
  206. # hash[:b] = 'y'
  207. # hash.values_at('a', 'b') # => ["x", "y"]
  208. 1 def values_at(*keys)
  209. super(*keys.map { |key| convert_key(key) })
  210. end
  211. # Returns an array of the values at the specified indices, but also
  212. # raises an exception when one of the keys can't be found.
  213. #
  214. # hash = ActiveSupport::HashWithIndifferentAccess.new
  215. # hash[:a] = 'x'
  216. # hash[:b] = 'y'
  217. # hash.fetch_values('a', 'b') # => ["x", "y"]
  218. # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
  219. # hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
  220. 1 def fetch_values(*indices, &block)
  221. super(*indices.map { |key| convert_key(key) }, &block)
  222. end
  223. # Returns a shallow copy of the hash.
  224. #
  225. # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
  226. # dup = hash.dup
  227. # dup[:a][:c] = 'c'
  228. #
  229. # hash[:a][:c] # => "c"
  230. # dup[:a][:c] # => "c"
  231. 1 def dup
  232. self.class.new(self).tap do |new_hash|
  233. set_defaults(new_hash)
  234. end
  235. end
  236. # This method has the same semantics of +update+, except it does not
  237. # modify the receiver but rather returns a new hash with indifferent
  238. # access with the result of the merge.
  239. 1 def merge(*hashes, &block)
  240. dup.update(*hashes, &block)
  241. end
  242. # Like +merge+ but the other way around: Merges the receiver into the
  243. # argument and returns a new hash with indifferent access as result:
  244. #
  245. # hash = ActiveSupport::HashWithIndifferentAccess.new
  246. # hash['a'] = nil
  247. # hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
  248. 1 def reverse_merge(other_hash)
  249. super(self.class.new(other_hash))
  250. end
  251. 1 alias_method :with_defaults, :reverse_merge
  252. # Same semantics as +reverse_merge+ but modifies the receiver in-place.
  253. 1 def reverse_merge!(other_hash)
  254. super(self.class.new(other_hash))
  255. end
  256. 1 alias_method :with_defaults!, :reverse_merge!
  257. # Replaces the contents of this hash with other_hash.
  258. #
  259. # h = { "a" => 100, "b" => 200 }
  260. # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
  261. 1 def replace(other_hash)
  262. super(self.class.new(other_hash))
  263. end
  264. # Removes the specified key from the hash.
  265. 1 def delete(key)
  266. super(convert_key(key))
  267. end
  268. 1 def except(*keys)
  269. slice(*self.keys - keys.map { |key| convert_key(key) })
  270. end
  271. 1 alias_method :without, :except
  272. 1 def stringify_keys!; self end
  273. 1 def deep_stringify_keys!; self end
  274. 1 def stringify_keys; dup end
  275. 1 def deep_stringify_keys; dup end
  276. 1 undef :symbolize_keys!
  277. 1 undef :deep_symbolize_keys!
  278. 1 def symbolize_keys; to_hash.symbolize_keys! end
  279. 1 alias_method :to_options, :symbolize_keys
  280. 1 def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
  281. 1 def to_options!; self end
  282. 1 def select(*args, &block)
  283. return to_enum(:select) unless block_given?
  284. dup.tap { |hash| hash.select!(*args, &block) }
  285. end
  286. 1 def reject(*args, &block)
  287. return to_enum(:reject) unless block_given?
  288. dup.tap { |hash| hash.reject!(*args, &block) }
  289. end
  290. 1 def transform_values(*args, &block)
  291. return to_enum(:transform_values) unless block_given?
  292. dup.tap { |hash| hash.transform_values!(*args, &block) }
  293. end
  294. 1 def transform_keys(*args, &block)
  295. return to_enum(:transform_keys) unless block_given?
  296. dup.tap { |hash| hash.transform_keys!(*args, &block) }
  297. end
  298. 1 def transform_keys!
  299. return enum_for(:transform_keys!) { size } unless block_given?
  300. keys.each do |key|
  301. self[yield(key)] = delete(key)
  302. end
  303. self
  304. end
  305. 1 def slice(*keys)
  306. keys.map! { |key| convert_key(key) }
  307. self.class.new(super)
  308. end
  309. 1 def slice!(*keys)
  310. keys.map! { |key| convert_key(key) }
  311. super
  312. end
  313. 1 def compact
  314. dup.tap(&:compact!)
  315. end
  316. # Convert to a regular hash with string keys.
  317. 1 def to_hash
  318. _new_hash = Hash.new
  319. set_defaults(_new_hash)
  320. each do |key, value|
  321. _new_hash[key] = convert_value(value, conversion: :to_hash)
  322. end
  323. _new_hash
  324. end
  325. 1 private
  326. 1 def convert_key(key)
  327. key.kind_of?(Symbol) ? key.to_s : key
  328. end
  329. 1 def convert_value(value, conversion: nil)
  330. if value.is_a? Hash
  331. if conversion == :to_hash
  332. value.to_hash
  333. else
  334. value.nested_under_indifferent_access
  335. end
  336. elsif value.is_a?(Array)
  337. if conversion != :assignment || value.frozen?
  338. value = value.dup
  339. end
  340. value.map! { |e| convert_value(e, conversion: conversion) }
  341. else
  342. value
  343. end
  344. end
  345. 1 def set_defaults(target)
  346. if default_proc
  347. target.default_proc = default_proc.dup
  348. else
  349. target.default = default
  350. end
  351. end
  352. 1 def update_with_single_argument(other_hash, block)
  353. if other_hash.is_a? HashWithIndifferentAccess
  354. regular_update(other_hash, &block)
  355. else
  356. other_hash.to_hash.each_pair do |key, value|
  357. if block && key?(key)
  358. value = block.call(convert_key(key), self[key], value)
  359. end
  360. regular_writer(convert_key(key), convert_value(value))
  361. end
  362. end
  363. end
  364. end
  365. end
  366. # :stopdoc:
  367. 1 HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess

lib/active_support/i18n.rb

81.82% lines covered

11 relevant lines. 9 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/core_ext/hash/deep_merge"
  3. 24 require "active_support/core_ext/hash/except"
  4. 24 require "active_support/core_ext/hash/slice"
  5. 24 begin
  6. 24 require "i18n"
  7. rescue LoadError => e
  8. $stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
  9. raise e
  10. end
  11. 24 require "active_support/lazy_load_hooks"
  12. 24 ActiveSupport.run_load_hooks(:i18n)
  13. 24 I18n.load_path << File.expand_path("locale/en.yml", __dir__)
  14. 24 I18n.load_path << File.expand_path("locale/en.rb", __dir__)

lib/active_support/i18n_railtie.rb

0.0% lines covered

103 relevant lines. 0 lines covered and 103 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support"
  3. require "active_support/core_ext/array/wrap"
  4. # :enddoc:
  5. module I18n
  6. class Railtie < Rails::Railtie
  7. config.i18n = ActiveSupport::OrderedOptions.new
  8. config.i18n.railties_load_path = []
  9. config.i18n.load_path = []
  10. config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
  11. config.eager_load_namespaces << I18n
  12. # Set the i18n configuration after initialization since a lot of
  13. # configuration is still usually done in application initializers.
  14. config.after_initialize do |app|
  15. I18n::Railtie.initialize_i18n(app)
  16. end
  17. # Trigger i18n config before any eager loading has happened
  18. # so it's ready if any classes require it when eager loaded.
  19. config.before_eager_load do |app|
  20. I18n::Railtie.initialize_i18n(app)
  21. end
  22. @i18n_inited = false
  23. # Setup i18n configuration.
  24. def self.initialize_i18n(app)
  25. return if @i18n_inited
  26. fallbacks = app.config.i18n.delete(:fallbacks)
  27. # Avoid issues with setting the default_locale by disabling available locales
  28. # check while configuring.
  29. enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
  30. enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
  31. I18n.enforce_available_locales = false
  32. reloadable_paths = []
  33. app.config.i18n.each do |setting, value|
  34. case setting
  35. when :railties_load_path
  36. reloadable_paths = value
  37. app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
  38. when :load_path
  39. I18n.load_path += value
  40. when :raise_on_missing_translations
  41. forward_raise_on_missing_translations_config(app)
  42. else
  43. I18n.send("#{setting}=", value)
  44. end
  45. end
  46. init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
  47. # Restore available locales check so it will take place from now on.
  48. I18n.enforce_available_locales = enforce_available_locales
  49. directories = watched_dirs_with_extensions(reloadable_paths)
  50. reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
  51. I18n.load_path.keep_if { |p| File.exist?(p) }
  52. I18n.load_path |= reloadable_paths.flat_map(&:existent)
  53. end
  54. app.reloaders << reloader
  55. app.reloader.to_run do
  56. reloader.execute_if_updated { require_unload_lock! }
  57. end
  58. reloader.execute
  59. @i18n_inited = true
  60. end
  61. def self.forward_raise_on_missing_translations_config(app)
  62. ActiveSupport.on_load(:action_view) do
  63. self.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
  64. end
  65. ActiveSupport.on_load(:action_controller) do
  66. AbstractController::Translation.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
  67. end
  68. end
  69. def self.include_fallbacks_module
  70. I18n.backend.class.include(I18n::Backend::Fallbacks)
  71. end
  72. def self.init_fallbacks(fallbacks)
  73. include_fallbacks_module
  74. args = \
  75. case fallbacks
  76. when ActiveSupport::OrderedOptions
  77. [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
  78. when Hash, Array
  79. Array.wrap(fallbacks)
  80. else # TrueClass
  81. [I18n.default_locale]
  82. end
  83. if args.empty? || args.first.is_a?(Hash)
  84. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  85. Using I18n fallbacks with an empty `defaults` sets the defaults to
  86. include the `default_locale`. This behavior will change in Rails 6.1.
  87. If you desire the default locale to be included in the defaults, please
  88. explicitly configure it with `config.i18n.fallbacks.defaults =
  89. [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale,
  90. {...}]`. If you want to opt-in to the new behavior, use
  91. `config.i18n.fallbacks.defaults = [nil, {...}]`.
  92. MSG
  93. args.unshift I18n.default_locale
  94. end
  95. I18n.fallbacks = I18n::Locale::Fallbacks.new(*args)
  96. end
  97. def self.validate_fallbacks(fallbacks)
  98. case fallbacks
  99. when ActiveSupport::OrderedOptions
  100. !fallbacks.empty?
  101. when TrueClass, Array, Hash
  102. true
  103. else
  104. raise "Unexpected fallback type #{fallbacks.inspect}"
  105. end
  106. end
  107. def self.watched_dirs_with_extensions(paths)
  108. paths.each_with_object({}) do |path, result|
  109. result[path.absolute_current] = path.extensions
  110. end
  111. end
  112. end
  113. end

lib/active_support/inflections.rb

100.0% lines covered

58 relevant lines. 58 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/inflector/inflections"
  3. #--
  4. # Defines the standard inflection rules. These are the starting point for
  5. # new projects and are not considered complete. The current set of inflection
  6. # rules is frozen. This means, we do not change them to become more complete.
  7. # This is a safety measure to keep existing applications from breaking.
  8. #++
  9. 24 module ActiveSupport
  10. 24 Inflector.inflections(:en) do |inflect|
  11. 24 inflect.plural(/$/, "s")
  12. 24 inflect.plural(/s$/i, "s")
  13. 24 inflect.plural(/^(ax|test)is$/i, '\1es')
  14. 24 inflect.plural(/(octop|vir)us$/i, '\1i')
  15. 24 inflect.plural(/(octop|vir)i$/i, '\1i')
  16. 24 inflect.plural(/(alias|status)$/i, '\1es')
  17. 24 inflect.plural(/(bu)s$/i, '\1ses')
  18. 24 inflect.plural(/(buffal|tomat)o$/i, '\1oes')
  19. 24 inflect.plural(/([ti])um$/i, '\1a')
  20. 24 inflect.plural(/([ti])a$/i, '\1a')
  21. 24 inflect.plural(/sis$/i, "ses")
  22. 24 inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
  23. 24 inflect.plural(/(hive)$/i, '\1s')
  24. 24 inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
  25. 24 inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
  26. 24 inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
  27. 24 inflect.plural(/^(m|l)ouse$/i, '\1ice')
  28. 24 inflect.plural(/^(m|l)ice$/i, '\1ice')
  29. 24 inflect.plural(/^(ox)$/i, '\1en')
  30. 24 inflect.plural(/^(oxen)$/i, '\1')
  31. 24 inflect.plural(/(quiz)$/i, '\1zes')
  32. 24 inflect.singular(/s$/i, "")
  33. 24 inflect.singular(/(ss)$/i, '\1')
  34. 24 inflect.singular(/(n)ews$/i, '\1ews')
  35. 24 inflect.singular(/([ti])a$/i, '\1um')
  36. 24 inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
  37. 24 inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
  38. 24 inflect.singular(/([^f])ves$/i, '\1fe')
  39. 24 inflect.singular(/(hive)s$/i, '\1')
  40. 24 inflect.singular(/(tive)s$/i, '\1')
  41. 24 inflect.singular(/([lr])ves$/i, '\1f')
  42. 24 inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
  43. 24 inflect.singular(/(s)eries$/i, '\1eries')
  44. 24 inflect.singular(/(m)ovies$/i, '\1ovie')
  45. 24 inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
  46. 24 inflect.singular(/^(m|l)ice$/i, '\1ouse')
  47. 24 inflect.singular(/(bus)(es)?$/i, '\1')
  48. 24 inflect.singular(/(o)es$/i, '\1')
  49. 24 inflect.singular(/(shoe)s$/i, '\1')
  50. 24 inflect.singular(/(cris|test)(is|es)$/i, '\1is')
  51. 24 inflect.singular(/^(a)x[ie]s$/i, '\1xis')
  52. 24 inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
  53. 24 inflect.singular(/(alias|status)(es)?$/i, '\1')
  54. 24 inflect.singular(/^(ox)en/i, '\1')
  55. 24 inflect.singular(/(vert|ind)ices$/i, '\1ex')
  56. 24 inflect.singular(/(matr)ices$/i, '\1ix')
  57. 24 inflect.singular(/(quiz)zes$/i, '\1')
  58. 24 inflect.singular(/(database)s$/i, '\1')
  59. 24 inflect.irregular("person", "people")
  60. 24 inflect.irregular("man", "men")
  61. 24 inflect.irregular("child", "children")
  62. 24 inflect.irregular("sex", "sexes")
  63. 24 inflect.irregular("move", "moves")
  64. 24 inflect.irregular("zombie", "zombies")
  65. 24 inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
  66. end
  67. end

lib/active_support/inflector.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # in case active_support/inflector is required without the rest of active_support
  3. 23 require "active_support/inflector/inflections"
  4. 23 require "active_support/inflector/transliterate"
  5. 23 require "active_support/inflector/methods"
  6. 23 require "active_support/inflections"
  7. 23 require "active_support/core_ext/string/inflections"

lib/active_support/inflector/inflections.rb

77.65% lines covered

85 relevant lines. 66 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "concurrent/map"
  3. 24 require "active_support/i18n"
  4. 24 module ActiveSupport
  5. 24 module Inflector
  6. 24 extend self
  7. # A singleton instance of this class is yielded by Inflector.inflections,
  8. # which can then be used to specify additional inflection rules. If passed
  9. # an optional locale, rules for other languages can be specified. The
  10. # default locale is <tt>:en</tt>. Only rules for English are provided.
  11. #
  12. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  13. # inflect.plural /^(ox)$/i, '\1\2en'
  14. # inflect.singular /^(ox)en/i, '\1'
  15. #
  16. # inflect.irregular 'octopus', 'octopi'
  17. #
  18. # inflect.uncountable 'equipment'
  19. # end
  20. #
  21. # New rules are added at the top. So in the example above, the irregular
  22. # rule for octopus will now be the first of the pluralization and
  23. # singularization rules that is runs. This guarantees that your rules run
  24. # before any of the rules that may already have been loaded.
  25. 24 class Inflections
  26. 24 @__instance__ = Concurrent::Map.new
  27. 24 class Uncountables < Array
  28. 24 def initialize
  29. 24 @regex_array = []
  30. 24 super
  31. end
  32. 24 def delete(entry)
  33. 2016 super entry
  34. 2016 @regex_array.delete(to_regex(entry))
  35. end
  36. 24 def <<(*word)
  37. add(word)
  38. end
  39. 24 def add(words)
  40. 25 words = words.flatten.map(&:downcase)
  41. 25 concat(words)
  42. 265 @regex_array += words.map { |word| to_regex(word) }
  43. 25 self
  44. end
  45. 24 def uncountable?(str)
  46. @regex_array.any? { |regex| regex.match? str }
  47. end
  48. 24 private
  49. 24 def to_regex(string)
  50. 2256 /\b#{::Regexp.escape(string)}\Z/i
  51. end
  52. end
  53. 24 def self.instance(locale = :en)
  54. 1753 @__instance__[locale] ||= new
  55. end
  56. 24 attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms
  57. 24 attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
  58. 24 def initialize
  59. 24 @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
  60. 24 define_acronym_regex_patterns
  61. end
  62. # Private, for the test suite.
  63. 24 def initialize_dup(orig) # :nodoc:
  64. %w(plurals singulars uncountables humans acronyms).each do |scope|
  65. instance_variable_set("@#{scope}", orig.send(scope).dup)
  66. end
  67. define_acronym_regex_patterns
  68. end
  69. # Specifies a new acronym. An acronym must be specified as it will appear
  70. # in a camelized string. An underscore string that contains the acronym
  71. # will retain the acronym when passed to +camelize+, +humanize+, or
  72. # +titleize+. A camelized string that contains the acronym will maintain
  73. # the acronym when titleized or humanized, and will convert the acronym
  74. # into a non-delimited single lowercase word when passed to +underscore+.
  75. #
  76. # acronym 'HTML'
  77. # titleize 'html' # => 'HTML'
  78. # camelize 'html' # => 'HTML'
  79. # underscore 'MyHTML' # => 'my_html'
  80. #
  81. # The acronym, however, must occur as a delimited unit and not be part of
  82. # another word for conversions to recognize it:
  83. #
  84. # acronym 'HTTP'
  85. # camelize 'my_http_delimited' # => 'MyHTTPDelimited'
  86. # camelize 'https' # => 'Https', not 'HTTPs'
  87. # underscore 'HTTPS' # => 'http_s', not 'https'
  88. #
  89. # acronym 'HTTPS'
  90. # camelize 'https' # => 'HTTPS'
  91. # underscore 'HTTPS' # => 'https'
  92. #
  93. # Note: Acronyms that are passed to +pluralize+ will no longer be
  94. # recognized, since the acronym will not occur as a delimited unit in the
  95. # pluralized result. To work around this, you must specify the pluralized
  96. # form as an acronym as well:
  97. #
  98. # acronym 'API'
  99. # camelize(pluralize('api')) # => 'Apis'
  100. #
  101. # acronym 'APIs'
  102. # camelize(pluralize('api')) # => 'APIs'
  103. #
  104. # +acronym+ may be used to specify any word that contains an acronym or
  105. # otherwise needs to maintain a non-standard capitalization. The only
  106. # restriction is that the word must begin with a capital letter.
  107. #
  108. # acronym 'RESTful'
  109. # underscore 'RESTful' # => 'restful'
  110. # underscore 'RESTfulController' # => 'restful_controller'
  111. # titleize 'RESTfulController' # => 'RESTful Controller'
  112. # camelize 'restful' # => 'RESTful'
  113. # camelize 'restful_controller' # => 'RESTfulController'
  114. #
  115. # acronym 'McDonald'
  116. # underscore 'McDonald' # => 'mcdonald'
  117. # camelize 'mcdonald' # => 'McDonald'
  118. 24 def acronym(word)
  119. @acronyms[word.downcase] = word
  120. define_acronym_regex_patterns
  121. end
  122. # Specifies a new pluralization rule and its replacement. The rule can
  123. # either be a string or a regular expression. The replacement should
  124. # always be a string that may include references to the matched data from
  125. # the rule.
  126. 24 def plural(rule, replacement)
  127. 792 @uncountables.delete(rule) if rule.is_a?(String)
  128. 792 @uncountables.delete(replacement)
  129. 792 @plurals.prepend([rule, replacement])
  130. end
  131. # Specifies a new singularization rule and its replacement. The rule can
  132. # either be a string or a regular expression. The replacement should
  133. # always be a string that may include references to the matched data from
  134. # the rule.
  135. 24 def singular(rule, replacement)
  136. 936 @uncountables.delete(rule) if rule.is_a?(String)
  137. 936 @uncountables.delete(replacement)
  138. 936 @singulars.prepend([rule, replacement])
  139. end
  140. # Specifies a new irregular that applies to both pluralization and
  141. # singularization at the same time. This can only be used for strings, not
  142. # regular expressions. You simply pass the irregular in singular and
  143. # plural form.
  144. #
  145. # irregular 'octopus', 'octopi'
  146. # irregular 'person', 'people'
  147. 24 def irregular(singular, plural)
  148. 144 @uncountables.delete(singular)
  149. 144 @uncountables.delete(plural)
  150. 144 s0 = singular[0]
  151. 144 srest = singular[1..-1]
  152. 144 p0 = plural[0]
  153. 144 prest = plural[1..-1]
  154. 144 if s0.upcase == p0.upcase
  155. 144 plural(/(#{s0})#{srest}$/i, '\1' + prest)
  156. 144 plural(/(#{p0})#{prest}$/i, '\1' + prest)
  157. 144 singular(/(#{s0})#{srest}$/i, '\1' + srest)
  158. 144 singular(/(#{p0})#{prest}$/i, '\1' + srest)
  159. else
  160. plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
  161. plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
  162. plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
  163. plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
  164. singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
  165. singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
  166. singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
  167. singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
  168. end
  169. end
  170. # Specifies words that are uncountable and should not be inflected.
  171. #
  172. # uncountable 'money'
  173. # uncountable 'money', 'information'
  174. # uncountable %w( money information rice )
  175. 24 def uncountable(*words)
  176. 25 @uncountables.add(words)
  177. end
  178. # Specifies a humanized form of a string by a regular expression rule or
  179. # by a string mapping. When using a regular expression based replacement,
  180. # the normal humanize formatting is called after the replacement. When a
  181. # string is used, the human form should be specified as desired (example:
  182. # 'The name', not 'the_name').
  183. #
  184. # human /_cnt$/i, '\1_count'
  185. # human 'legacy_col_person_name', 'Name'
  186. 24 def human(rule, replacement)
  187. @humans.prepend([rule, replacement])
  188. end
  189. # Clears the loaded inflections within a given scope (default is
  190. # <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
  191. # options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
  192. # <tt>:humans</tt>.
  193. #
  194. # clear :all
  195. # clear :plurals
  196. 24 def clear(scope = :all)
  197. case scope
  198. when :all
  199. @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
  200. else
  201. instance_variable_set "@#{scope}", []
  202. end
  203. end
  204. 24 private
  205. 24 def define_acronym_regex_patterns
  206. 24 @acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
  207. 24 @acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
  208. 24 @acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
  209. end
  210. end
  211. # Yields a singleton instance of Inflector::Inflections so you can specify
  212. # additional inflector rules. If passed an optional locale, rules for other
  213. # languages can be specified. If not specified, defaults to <tt>:en</tt>.
  214. # Only rules for English are provided.
  215. #
  216. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  217. # inflect.uncountable 'rails'
  218. # end
  219. 24 def inflections(locale = :en)
  220. 1753 if block_given?
  221. 26 yield Inflections.instance(locale)
  222. else
  223. 1727 Inflections.instance(locale)
  224. end
  225. end
  226. end
  227. end

lib/active_support/inflector/methods.rb

40.21% lines covered

97 relevant lines. 39 lines covered and 58 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/inflections"
  3. 24 require "active_support/core_ext/object/blank"
  4. 24 module ActiveSupport
  5. # The Inflector transforms words from singular to plural, class names to table
  6. # names, modularized class names to ones without, and class names to foreign
  7. # keys. The default inflections for pluralization, singularization, and
  8. # uncountable words are kept in inflections.rb.
  9. #
  10. # The Rails core team has stated patches for the inflections library will not
  11. # be accepted in order to avoid breaking legacy applications which may be
  12. # relying on errant inflections. If you discover an incorrect inflection and
  13. # require it for your application or wish to define rules for languages other
  14. # than English, please correct or add them yourself (explained below).
  15. 24 module Inflector
  16. 24 extend self
  17. # Returns the plural form of the word in the string.
  18. #
  19. # If passed an optional +locale+ parameter, the word will be
  20. # pluralized using rules defined for that language. By default,
  21. # this parameter is set to <tt>:en</tt>.
  22. #
  23. # pluralize('post') # => "posts"
  24. # pluralize('octopus') # => "octopi"
  25. # pluralize('sheep') # => "sheep"
  26. # pluralize('words') # => "words"
  27. # pluralize('CamelOctopus') # => "CamelOctopi"
  28. # pluralize('ley', :es) # => "leyes"
  29. 24 def pluralize(word, locale = :en)
  30. apply_inflections(word, inflections(locale).plurals, locale)
  31. end
  32. # The reverse of #pluralize, returns the singular form of a word in a
  33. # string.
  34. #
  35. # If passed an optional +locale+ parameter, the word will be
  36. # singularized using rules defined for that language. By default,
  37. # this parameter is set to <tt>:en</tt>.
  38. #
  39. # singularize('posts') # => "post"
  40. # singularize('octopi') # => "octopus"
  41. # singularize('sheep') # => "sheep"
  42. # singularize('word') # => "word"
  43. # singularize('CamelOctopi') # => "CamelOctopus"
  44. # singularize('leyes', :es) # => "ley"
  45. 24 def singularize(word, locale = :en)
  46. apply_inflections(word, inflections(locale).singulars, locale)
  47. end
  48. # Converts strings to UpperCamelCase.
  49. # If the +uppercase_first_letter+ parameter is set to false, then produces
  50. # lowerCamelCase.
  51. #
  52. # Also converts '/' to '::' which is useful for converting
  53. # paths to namespaces.
  54. #
  55. # camelize('active_model') # => "ActiveModel"
  56. # camelize('active_model', false) # => "activeModel"
  57. # camelize('active_model/errors') # => "ActiveModel::Errors"
  58. # camelize('active_model/errors', false) # => "activeModel::Errors"
  59. #
  60. # As a rule of thumb you can think of +camelize+ as the inverse of
  61. # #underscore, though there are cases where that does not hold:
  62. #
  63. # camelize(underscore('SSLError')) # => "SslError"
  64. 24 def camelize(term, uppercase_first_letter = true)
  65. 3 string = term.to_s
  66. 3 if uppercase_first_letter
  67. 6 string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
  68. else
  69. string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
  70. end
  71. 3 string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
  72. 3 string.gsub!("/", "::")
  73. 3 string
  74. end
  75. # Makes an underscored, lowercase form from the expression in the string.
  76. #
  77. # Changes '::' to '/' to convert namespaces to paths.
  78. #
  79. # underscore('ActiveModel') # => "active_model"
  80. # underscore('ActiveModel::Errors') # => "active_model/errors"
  81. #
  82. # As a rule of thumb you can think of +underscore+ as the inverse of
  83. # #camelize, though there are cases where that does not hold:
  84. #
  85. # camelize(underscore('SSLError')) # => "SslError"
  86. 24 def underscore(camel_cased_word)
  87. 1723 return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
  88. 1723 word = camel_cased_word.to_s.gsub("::", "/")
  89. 1723 word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
  90. 1723 word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
  91. 1723 word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
  92. 1723 word.tr!("-", "_")
  93. 1723 word.downcase!
  94. 1723 word
  95. end
  96. # Tweaks an attribute name for display to end users.
  97. #
  98. # Specifically, performs these transformations:
  99. #
  100. # * Applies human inflection rules to the argument.
  101. # * Deletes leading underscores, if any.
  102. # * Removes a "_id" suffix if present.
  103. # * Replaces underscores with spaces, if any.
  104. # * Downcases all words except acronyms.
  105. # * Capitalizes the first word.
  106. # The capitalization of the first word can be turned off by setting the
  107. # +:capitalize+ option to false (default is true).
  108. #
  109. # The trailing '_id' can be kept and capitalized by setting the
  110. # optional parameter +keep_id_suffix+ to true (default is false).
  111. #
  112. # humanize('employee_salary') # => "Employee salary"
  113. # humanize('author_id') # => "Author"
  114. # humanize('author_id', capitalize: false) # => "author"
  115. # humanize('_id') # => "Id"
  116. # humanize('author_id', keep_id_suffix: true) # => "Author Id"
  117. #
  118. # If "SSL" was defined to be an acronym:
  119. #
  120. # humanize('ssl_error') # => "SSL error"
  121. #
  122. 24 def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
  123. result = lower_case_and_underscored_word.to_s.dup
  124. inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
  125. result.sub!(/\A_+/, "")
  126. unless keep_id_suffix
  127. result.delete_suffix!("_id")
  128. end
  129. result.tr!("_", " ")
  130. result.gsub!(/([a-z\d]*)/i) do |match|
  131. "#{inflections.acronyms[match.downcase] || match.downcase}"
  132. end
  133. if capitalize
  134. result.sub!(/\A\w/) { |match| match.upcase }
  135. end
  136. result
  137. end
  138. # Converts just the first character to uppercase.
  139. #
  140. # upcase_first('what a Lovely Day') # => "What a Lovely Day"
  141. # upcase_first('w') # => "W"
  142. # upcase_first('') # => ""
  143. 24 def upcase_first(string)
  144. string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
  145. end
  146. # Capitalizes all the words and replaces some characters in the string to
  147. # create a nicer looking title. +titleize+ is meant for creating pretty
  148. # output. It is not used in the Rails internals.
  149. #
  150. # The trailing '_id','Id'.. can be kept and capitalized by setting the
  151. # optional parameter +keep_id_suffix+ to true.
  152. # By default, this parameter is false.
  153. #
  154. # +titleize+ is also aliased as +titlecase+.
  155. #
  156. # titleize('man from the boondocks') # => "Man From The Boondocks"
  157. # titleize('x-men: the last stand') # => "X Men: The Last Stand"
  158. # titleize('TheManWithoutAPast') # => "The Man Without A Past"
  159. # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
  160. # titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
  161. 24 def titleize(word, keep_id_suffix: false)
  162. humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`()])[a-z]/) do |match|
  163. match.capitalize
  164. end
  165. end
  166. # Creates the name of a table like Rails does for models to table names.
  167. # This method uses the #pluralize method on the last word in the string.
  168. #
  169. # tableize('RawScaledScorer') # => "raw_scaled_scorers"
  170. # tableize('ham_and_egg') # => "ham_and_eggs"
  171. # tableize('fancyCategory') # => "fancy_categories"
  172. 24 def tableize(class_name)
  173. pluralize(underscore(class_name))
  174. end
  175. # Creates a class name from a plural table name like Rails does for table
  176. # names to models. Note that this returns a string and not a Class (To
  177. # convert to an actual class follow +classify+ with #constantize).
  178. #
  179. # classify('ham_and_eggs') # => "HamAndEgg"
  180. # classify('posts') # => "Post"
  181. #
  182. # Singular names are not handled correctly:
  183. #
  184. # classify('calculus') # => "Calculu"
  185. 24 def classify(table_name)
  186. # strip out any leading schema name
  187. camelize(singularize(table_name.to_s.sub(/.*\./, "")))
  188. end
  189. # Replaces underscores with dashes in the string.
  190. #
  191. # dasherize('puni_puni') # => "puni-puni"
  192. 24 def dasherize(underscored_word)
  193. underscored_word.tr("_", "-")
  194. end
  195. # Removes the module part from the expression in the string.
  196. #
  197. # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
  198. # demodulize('Inflections') # => "Inflections"
  199. # demodulize('::Inflections') # => "Inflections"
  200. # demodulize('') # => ""
  201. #
  202. # See also #deconstantize.
  203. 24 def demodulize(path)
  204. path = path.to_s
  205. if i = path.rindex("::")
  206. path[(i + 2)..-1]
  207. else
  208. path
  209. end
  210. end
  211. # Removes the rightmost segment from the constant expression in the string.
  212. #
  213. # deconstantize('Net::HTTP') # => "Net"
  214. # deconstantize('::Net::HTTP') # => "::Net"
  215. # deconstantize('String') # => ""
  216. # deconstantize('::String') # => ""
  217. # deconstantize('') # => ""
  218. #
  219. # See also #demodulize.
  220. 24 def deconstantize(path)
  221. path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
  222. end
  223. # Creates a foreign key name from a class name.
  224. # +separate_class_name_and_id_with_underscore+ sets whether
  225. # the method should put '_' between the name and 'id'.
  226. #
  227. # foreign_key('Message') # => "message_id"
  228. # foreign_key('Message', false) # => "messageid"
  229. # foreign_key('Admin::Post') # => "post_id"
  230. 24 def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
  231. underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
  232. end
  233. # Tries to find a constant with the name specified in the argument string.
  234. #
  235. # constantize('Module') # => Module
  236. # constantize('Foo::Bar') # => Foo::Bar
  237. #
  238. # The name is assumed to be the one of a top-level constant, no matter
  239. # whether it starts with "::" or not. No lexical context is taken into
  240. # account:
  241. #
  242. # C = 'outside'
  243. # module M
  244. # C = 'inside'
  245. # C # => 'inside'
  246. # constantize('C') # => 'outside', same as ::C
  247. # end
  248. #
  249. # NameError is raised when the name is not in CamelCase or the constant is
  250. # unknown.
  251. 24 def constantize(camel_cased_word)
  252. if camel_cased_word.blank? || !camel_cased_word.include?("::")
  253. Object.const_get(camel_cased_word)
  254. else
  255. names = camel_cased_word.split("::")
  256. # Trigger a built-in NameError exception including the ill-formed constant in the message.
  257. Object.const_get(camel_cased_word) if names.empty?
  258. # Remove the first blank element in case of '::ClassName' notation.
  259. names.shift if names.size > 1 && names.first.empty?
  260. names.inject(Object) do |constant, name|
  261. if constant == Object
  262. constant.const_get(name)
  263. else
  264. candidate = constant.const_get(name)
  265. next candidate if constant.const_defined?(name, false)
  266. next candidate unless Object.const_defined?(name)
  267. # Go down the ancestors to check if it is owned directly. The check
  268. # stops when we reach Object or the end of ancestors tree.
  269. constant = constant.ancestors.inject(constant) do |const, ancestor|
  270. break const if ancestor == Object
  271. break ancestor if ancestor.const_defined?(name, false)
  272. const
  273. end
  274. # owner is in Object, so raise
  275. constant.const_get(name, false)
  276. end
  277. end
  278. end
  279. end
  280. # Tries to find a constant with the name specified in the argument string.
  281. #
  282. # safe_constantize('Module') # => Module
  283. # safe_constantize('Foo::Bar') # => Foo::Bar
  284. #
  285. # The name is assumed to be the one of a top-level constant, no matter
  286. # whether it starts with "::" or not. No lexical context is taken into
  287. # account:
  288. #
  289. # C = 'outside'
  290. # module M
  291. # C = 'inside'
  292. # C # => 'inside'
  293. # safe_constantize('C') # => 'outside', same as ::C
  294. # end
  295. #
  296. # +nil+ is returned when the name is not in CamelCase or the constant (or
  297. # part of it) is unknown.
  298. #
  299. # safe_constantize('blargle') # => nil
  300. # safe_constantize('UnknownModule') # => nil
  301. # safe_constantize('UnknownModule::Foo::Bar') # => nil
  302. 24 def safe_constantize(camel_cased_word)
  303. constantize(camel_cased_word)
  304. rescue NameError => e
  305. raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
  306. e.name.to_s == camel_cased_word.to_s)
  307. rescue LoadError => e
  308. message = e.respond_to?(:original_message) ? e.original_message : e.message
  309. raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(message)
  310. end
  311. # Returns the suffix that should be added to a number to denote the position
  312. # in an ordered sequence such as 1st, 2nd, 3rd, 4th.
  313. #
  314. # ordinal(1) # => "st"
  315. # ordinal(2) # => "nd"
  316. # ordinal(1002) # => "nd"
  317. # ordinal(1003) # => "rd"
  318. # ordinal(-11) # => "th"
  319. # ordinal(-1021) # => "st"
  320. 24 def ordinal(number)
  321. I18n.translate("number.nth.ordinals", number: number)
  322. end
  323. # Turns a number into an ordinal string used to denote the position in an
  324. # ordered sequence such as 1st, 2nd, 3rd, 4th.
  325. #
  326. # ordinalize(1) # => "1st"
  327. # ordinalize(2) # => "2nd"
  328. # ordinalize(1002) # => "1002nd"
  329. # ordinalize(1003) # => "1003rd"
  330. # ordinalize(-11) # => "-11th"
  331. # ordinalize(-1021) # => "-1021st"
  332. 24 def ordinalize(number)
  333. I18n.translate("number.nth.ordinalized", number: number)
  334. end
  335. 24 private
  336. # Mounts a regular expression, returned as a string to ease interpolation,
  337. # that will match part by part the given constant.
  338. #
  339. # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
  340. # const_regexp("::") # => "::"
  341. 24 def const_regexp(camel_cased_word)
  342. parts = camel_cased_word.split("::")
  343. return Regexp.escape(camel_cased_word) if parts.blank?
  344. last = parts.pop
  345. parts.reverse!.inject(last) do |acc, part|
  346. part.empty? ? acc : "#{part}(::#{acc})?"
  347. end
  348. end
  349. # Applies inflection rules for +singularize+ and +pluralize+.
  350. #
  351. # If passed an optional +locale+ parameter, the uncountables will be
  352. # found for that locale.
  353. #
  354. # apply_inflections('post', inflections.plurals, :en) # => "posts"
  355. # apply_inflections('posts', inflections.singulars, :en) # => "post"
  356. 24 def apply_inflections(word, rules, locale = :en)
  357. result = word.to_s.dup
  358. if word.empty? || inflections(locale).uncountables.uncountable?(result)
  359. result
  360. else
  361. rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
  362. result
  363. end
  364. end
  365. end
  366. end

lib/active_support/inflector/transliterate.rb

24.14% lines covered

29 relevant lines. 7 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/string/multibyte"
  3. 23 require "active_support/i18n"
  4. 23 module ActiveSupport
  5. 23 module Inflector
  6. 23 ALLOWED_ENCODINGS_FOR_TRANSLITERATE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030].freeze
  7. # Replaces non-ASCII characters with an ASCII approximation, or if none
  8. # exists, a replacement character which defaults to "?".
  9. #
  10. # transliterate('Ærøskøbing')
  11. # # => "AEroskobing"
  12. #
  13. # Default approximations are provided for Western/Latin characters,
  14. # e.g, "ø", "ñ", "é", "ß", etc.
  15. #
  16. # This method is I18n aware, so you can set up custom approximations for a
  17. # locale. This can be useful, for example, to transliterate German's "ü"
  18. # and "ö" to "ue" and "oe", or to add support for transliterating Russian
  19. # to ASCII.
  20. #
  21. # In order to make your custom transliterations available, you must set
  22. # them as the <tt>i18n.transliterate.rule</tt> i18n key:
  23. #
  24. # # Store the transliterations in locales/de.yml
  25. # i18n:
  26. # transliterate:
  27. # rule:
  28. # ü: "ue"
  29. # ö: "oe"
  30. #
  31. # # Or set them using Ruby
  32. # I18n.backend.store_translations(:de, i18n: {
  33. # transliterate: {
  34. # rule: {
  35. # 'ü' => 'ue',
  36. # 'ö' => 'oe'
  37. # }
  38. # }
  39. # })
  40. #
  41. # The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that
  42. # maps characters to ASCII approximations as shown above, or, for more
  43. # complex requirements, a Proc:
  44. #
  45. # I18n.backend.store_translations(:de, i18n: {
  46. # transliterate: {
  47. # rule: ->(string) { MyTransliterator.transliterate(string) }
  48. # }
  49. # })
  50. #
  51. # Now you can have different transliterations for each locale:
  52. #
  53. # transliterate('Jürgen', locale: :en)
  54. # # => "Jurgen"
  55. #
  56. # transliterate('Jürgen', locale: :de)
  57. # # => "Juergen"
  58. #
  59. # Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings
  60. # Other encodings will raise an ArgumentError.
  61. 23 def transliterate(string, replacement = "?", locale: nil)
  62. string = string.dup if string.frozen?
  63. raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
  64. raise ArgumentError, "Cannot transliterate strings with #{string.encoding} encoding" unless ALLOWED_ENCODINGS_FOR_TRANSLITERATE.include?(string.encoding)
  65. input_encoding = string.encoding
  66. # US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if
  67. # US-ASCII is given. This way we can let tidy_bytes handle the string
  68. # in the same way as we do for UTF-8
  69. string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII
  70. # GB18030 is Unicode compatible but is not a direct mapping so needs to be
  71. # transcoded. Using invalid/undef :replace will result in loss of data in
  72. # the event of invalid characters, but since tidy_bytes will replace
  73. # invalid/undef with a "?" we're safe to do the same beforehand
  74. string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030
  75. transliterated = I18n.transliterate(
  76. ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
  77. replacement: replacement,
  78. locale: locale
  79. )
  80. # Restore the string encoding of the input if it was not UTF-8.
  81. # Apply invalid/undef :replace as tidy_bytes does
  82. transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding
  83. transliterated
  84. end
  85. # Replaces special characters in a string so that it may be used as part of
  86. # a 'pretty' URL.
  87. #
  88. # parameterize("Donald E. Knuth") # => "donald-e-knuth"
  89. # parameterize("^très|Jolie-- ") # => "tres-jolie"
  90. #
  91. # To use a custom separator, override the +separator+ argument.
  92. #
  93. # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
  94. # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
  95. #
  96. # To preserve the case of the characters in a string, use the +preserve_case+ argument.
  97. #
  98. # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
  99. # parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
  100. #
  101. # It preserves dashes and underscores unless they are used as separators:
  102. #
  103. # parameterize("^très|Jolie__ ") # => "tres-jolie__"
  104. # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
  105. # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
  106. #
  107. # If the optional parameter +locale+ is specified,
  108. # the word will be parameterized as a word of that language.
  109. # By default, this parameter is set to <tt>nil</tt> and it will use
  110. # the configured <tt>I18n.locale</tt>.
  111. 23 def parameterize(string, separator: "-", preserve_case: false, locale: nil)
  112. # Replace accented chars with their ASCII equivalents.
  113. parameterized_string = transliterate(string, locale: locale)
  114. # Turn unwanted chars into the separator.
  115. parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
  116. unless separator.nil? || separator.empty?
  117. if separator == "-"
  118. re_duplicate_separator = /-{2,}/
  119. re_leading_trailing_separator = /^-|-$/i
  120. else
  121. re_sep = Regexp.escape(separator)
  122. re_duplicate_separator = /#{re_sep}{2,}/
  123. re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
  124. end
  125. # No more than one of the separator in a row.
  126. parameterized_string.gsub!(re_duplicate_separator, separator)
  127. # Remove leading/trailing separator.
  128. parameterized_string.gsub!(re_leading_trailing_separator, "")
  129. end
  130. parameterized_string.downcase! unless preserve_case
  131. parameterized_string
  132. end
  133. end
  134. end

lib/active_support/json.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/json/decoding"
  3. 2 require "active_support/json/encoding"

lib/active_support/json/decoding.rb

41.94% lines covered

31 relevant lines. 13 lines covered and 18 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/module/attribute_accessors"
  3. 2 require "active_support/core_ext/module/delegation"
  4. 2 require "json"
  5. 2 module ActiveSupport
  6. # Look for and parse json strings that look like ISO 8601 times.
  7. 2 mattr_accessor :parse_json_times
  8. 2 module JSON
  9. # matches YAML-formatted dates
  10. 2 DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
  11. 2 DATETIME_REGEX = /\A(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)\z/
  12. 2 class << self
  13. # Parses a JSON string (JavaScript Object Notation) into a hash.
  14. # See http://www.json.org for more info.
  15. #
  16. # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
  17. # => {"team" => "rails", "players" => "36"}
  18. 2 def decode(json)
  19. data = ::JSON.parse(json, quirks_mode: true)
  20. if ActiveSupport.parse_json_times
  21. convert_dates_from(data)
  22. else
  23. data
  24. end
  25. end
  26. # Returns the class of the error that will be raised when there is an
  27. # error in decoding JSON. Using this method means you won't directly
  28. # depend on the ActiveSupport's JSON implementation, in case it changes
  29. # in the future.
  30. #
  31. # begin
  32. # obj = ActiveSupport::JSON.decode(some_string)
  33. # rescue ActiveSupport::JSON.parse_error
  34. # Rails.logger.warn("Attempted to decode invalid JSON: #{some_string}")
  35. # end
  36. 2 def parse_error
  37. ::JSON::ParserError
  38. end
  39. 2 private
  40. 2 def convert_dates_from(data)
  41. case data
  42. when nil
  43. nil
  44. when DATE_REGEX
  45. begin
  46. Date.parse(data)
  47. rescue ArgumentError
  48. data
  49. end
  50. when DATETIME_REGEX
  51. begin
  52. Time.zone.parse(data)
  53. rescue ArgumentError
  54. data
  55. end
  56. when Array
  57. data.map! { |d| convert_dates_from(d) }
  58. when Hash
  59. data.each do |key, value|
  60. data[key] = convert_dates_from(value)
  61. end
  62. else
  63. data
  64. end
  65. end
  66. end
  67. end
  68. end

lib/active_support/json/encoding.rb

59.62% lines covered

52 relevant lines. 31 lines covered and 21 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/object/json"
  3. 2 require "active_support/core_ext/module/delegation"
  4. 2 module ActiveSupport
  5. 2 class << self
  6. 2 delegate :use_standard_json_time_format, :use_standard_json_time_format=,
  7. :time_precision, :time_precision=,
  8. :escape_html_entities_in_json, :escape_html_entities_in_json=,
  9. :json_encoder, :json_encoder=,
  10. to: :'ActiveSupport::JSON::Encoding'
  11. end
  12. 2 module JSON
  13. # Dumps objects in JSON (JavaScript Object Notation).
  14. # See http://www.json.org for more info.
  15. #
  16. # ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
  17. # # => "{\"team\":\"rails\",\"players\":\"36\"}"
  18. 2 def self.encode(value, options = nil)
  19. Encoding.json_encoder.new(options).encode(value)
  20. end
  21. 2 module Encoding #:nodoc:
  22. 2 class JSONGemEncoder #:nodoc:
  23. 2 attr_reader :options
  24. 2 def initialize(options = nil)
  25. @options = options || {}
  26. end
  27. # Encode the given object into a JSON string
  28. 2 def encode(value)
  29. stringify jsonify value.as_json(options.dup)
  30. end
  31. 2 private
  32. # Rails does more escaping than the JSON gem natively does (we
  33. # escape \u2028 and \u2029 and optionally >, <, & to work around
  34. # certain browser problems).
  35. 2 ESCAPED_CHARS = {
  36. "\u2028" => '\u2028',
  37. "\u2029" => '\u2029',
  38. ">" => '\u003e',
  39. "<" => '\u003c',
  40. "&" => '\u0026',
  41. }
  42. 2 ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
  43. 2 ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u
  44. # This class wraps all the strings we see and does the extra escaping
  45. 2 class EscapedString < String #:nodoc:
  46. 2 def to_json(*)
  47. if Encoding.escape_html_entities_in_json
  48. s = super
  49. s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
  50. s
  51. else
  52. s = super
  53. s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
  54. s
  55. end
  56. end
  57. 2 def to_s
  58. self
  59. end
  60. end
  61. # Mark these as private so we don't leak encoding-specific constructs
  62. 2 private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
  63. :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
  64. # Convert an object into a "JSON-ready" representation composed of
  65. # primitives like Hash, Array, String, Numeric,
  66. # and +true+/+false+/+nil+.
  67. # Recursively calls #as_json to the object to recursively build a
  68. # fully JSON-ready object.
  69. #
  70. # This allows developers to implement #as_json without having to
  71. # worry about what base types of objects they are allowed to return
  72. # or having to remember to call #as_json recursively.
  73. #
  74. # Note: the +options+ hash passed to +object.to_json+ is only passed
  75. # to +object.as_json+, not any of this method's recursive +#as_json+
  76. # calls.
  77. 2 def jsonify(value)
  78. case value
  79. when String
  80. EscapedString.new(value)
  81. when Numeric, NilClass, TrueClass, FalseClass
  82. value.as_json
  83. when Hash
  84. result = {}
  85. value.each do |k, v|
  86. result[jsonify(k)] = jsonify(v)
  87. end
  88. result
  89. when Array
  90. value.map { |v| jsonify(v) }
  91. else
  92. jsonify value.as_json
  93. end
  94. end
  95. # Encode a "jsonified" Ruby data structure using the JSON gem
  96. 2 def stringify(jsonified)
  97. ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
  98. end
  99. end
  100. 2 class << self
  101. # If true, use ISO 8601 format for dates and times. Otherwise, fall back
  102. # to the Active Support legacy format.
  103. 2 attr_accessor :use_standard_json_time_format
  104. # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
  105. # as a safety measure.
  106. 2 attr_accessor :escape_html_entities_in_json
  107. # Sets the precision of encoded time values.
  108. # Defaults to 3 (equivalent to millisecond precision)
  109. 2 attr_accessor :time_precision
  110. # Sets the encoder used by Rails to encode Ruby objects into JSON strings
  111. # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
  112. 2 attr_accessor :json_encoder
  113. end
  114. 2 self.use_standard_json_time_format = true
  115. 2 self.escape_html_entities_in_json = true
  116. 2 self.json_encoder = JSONGemEncoder
  117. 2 self.time_precision = 3
  118. end
  119. end
  120. end

lib/active_support/key_generator.rb

0.0% lines covered

22 relevant lines. 0 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. require "concurrent/map"
  3. require "openssl"
  4. module ActiveSupport
  5. # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2.
  6. # It can be used to derive a number of keys for various purposes from a given secret.
  7. # This lets Rails applications have a single secure secret, but avoid reusing that
  8. # key in multiple incompatible contexts.
  9. class KeyGenerator
  10. def initialize(secret, options = {})
  11. @secret = secret
  12. # The default iterations are higher than required for our key derivation uses
  13. # on the off chance someone uses this for password storage
  14. @iterations = options[:iterations] || 2**16
  15. end
  16. # Returns a derived key suitable for use. The default key_size is chosen
  17. # to be compatible with the default settings of ActiveSupport::MessageVerifier.
  18. # i.e. OpenSSL::Digest::SHA1#block_length
  19. def generate_key(salt, key_size = 64)
  20. OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
  21. end
  22. end
  23. # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
  24. # re-executing the key generation process when it's called using the same salt and
  25. # key_size.
  26. class CachingKeyGenerator
  27. def initialize(key_generator)
  28. @key_generator = key_generator
  29. @cache_keys = Concurrent::Map.new
  30. end
  31. # Returns a derived key suitable for use.
  32. def generate_key(*args)
  33. @cache_keys[args.join("|")] ||= @key_generator.generate_key(*args)
  34. end
  35. end
  36. end

lib/active_support/lazy_load_hooks.rb

53.57% lines covered

28 relevant lines. 15 lines covered and 13 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 module ActiveSupport
  3. # lazy_load_hooks allows Rails to lazily load a lot of components and thus
  4. # making the app boot faster. Because of this feature now there is no need to
  5. # require <tt>ActiveRecord::Base</tt> at boot time purely to apply
  6. # configuration. Instead a hook is registered that applies configuration once
  7. # <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is
  8. # used as example but this feature can be applied elsewhere too.
  9. #
  10. # Here is an example where +on_load+ method is called to register a hook.
  11. #
  12. # initializer 'active_record.initialize_timezone' do
  13. # ActiveSupport.on_load(:active_record) do
  14. # self.time_zone_aware_attributes = true
  15. # self.default_timezone = :utc
  16. # end
  17. # end
  18. #
  19. # When the entirety of +ActiveRecord::Base+ has been
  20. # evaluated then +run_load_hooks+ is invoked. The very last line of
  21. # +ActiveRecord::Base+ is:
  22. #
  23. # ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
  24. 24 module LazyLoadHooks
  25. 24 def self.extended(base) # :nodoc:
  26. 24 base.class_eval do
  27. 71 @load_hooks = Hash.new { |h, k| h[k] = [] }
  28. 71 @loaded = Hash.new { |h, k| h[k] = [] }
  29. 24 @run_once = Hash.new { |h, k| h[k] = [] }
  30. end
  31. end
  32. # Declares a block that will be executed when a Rails component is fully
  33. # loaded.
  34. #
  35. # Options:
  36. #
  37. # * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
  38. # * <tt>:run_once</tt> - Given +block+ will run only once.
  39. 24 def on_load(name, options = {}, &block)
  40. @loaded[name].each do |base|
  41. execute_hook(name, base, options, block)
  42. end
  43. @load_hooks[name] << [block, options]
  44. end
  45. 24 def run_load_hooks(name, base = Object)
  46. 47 @loaded[name] << base
  47. 47 @load_hooks[name].each do |hook, options|
  48. execute_hook(name, base, options, hook)
  49. end
  50. end
  51. 24 private
  52. 24 def with_execution_control(name, block, once)
  53. unless @run_once[name].include?(block)
  54. @run_once[name] << block if once
  55. yield
  56. end
  57. end
  58. 24 def execute_hook(name, base, options, block)
  59. with_execution_control(name, block, options[:run_once]) do
  60. if options[:yield]
  61. block.call(base)
  62. else
  63. if base.is_a?(Module)
  64. base.class_eval(&block)
  65. else
  66. base.instance_eval(&block)
  67. end
  68. end
  69. end
  70. end
  71. end
  72. 24 extend LazyLoadHooks
  73. end

lib/active_support/locale/en.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. # frozen_string_literal: true
  2. {
  3. en: {
  4. number: {
  5. nth: {
  6. ordinals: lambda do |_key, options|
  7. number = options[:number]
  8. case number
  9. when 1; "st"
  10. when 2; "nd"
  11. when 3; "rd"
  12. when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13; "th"
  13. else
  14. num_modulo = number.to_i.abs % 100
  15. num_modulo %= 10 if num_modulo > 13
  16. case num_modulo
  17. when 1; "st"
  18. when 2; "nd"
  19. when 3; "rd"
  20. else "th"
  21. end
  22. end
  23. end,
  24. ordinalized: lambda do |_key, options|
  25. number = options[:number]
  26. "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
  27. end
  28. }
  29. }
  30. }
  31. }

lib/active_support/log_subscriber.rb

68.29% lines covered

41 relevant lines. 28 lines covered and 13 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/attribute_accessors"
  3. 1 require "active_support/core_ext/class/attribute"
  4. 1 require "active_support/subscriber"
  5. 1 module ActiveSupport
  6. # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume
  7. # <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them.
  8. # The log subscriber dispatches notifications to a registered object based
  9. # on its given namespace.
  10. #
  11. # An example would be Active Record log subscriber responsible for logging
  12. # queries:
  13. #
  14. # module ActiveRecord
  15. # class LogSubscriber < ActiveSupport::LogSubscriber
  16. # def sql(event)
  17. # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
  18. # end
  19. # end
  20. # end
  21. #
  22. # And it's finally registered as:
  23. #
  24. # ActiveRecord::LogSubscriber.attach_to :active_record
  25. #
  26. # Since we need to know all instance methods before attaching the log
  27. # subscriber, the line above should be called after your
  28. # <tt>ActiveRecord::LogSubscriber</tt> definition.
  29. #
  30. # A logger also needs to be set with <tt>ActiveRecord::LogSubscriber.logger=</tt>.
  31. # This is assigned automatically in a Rails environment.
  32. #
  33. # After configured, whenever a <tt>"sql.active_record"</tt> notification is published,
  34. # it will properly dispatch the event
  35. # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method.
  36. #
  37. # Being an <tt>ActiveSupport::Notifications</tt> consumer,
  38. # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if
  39. # instrumented code raises an exception. It is common to log a different
  40. # message in case of an error, and this can be achieved by extending
  41. # the previous example:
  42. #
  43. # module ActiveRecord
  44. # class LogSubscriber < ActiveSupport::LogSubscriber
  45. # def sql(event)
  46. # exception = event.payload[:exception]
  47. #
  48. # if exception
  49. # exception_object = event.payload[:exception_object]
  50. #
  51. # error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \
  52. # "(#{exception_object.backtrace.first})"
  53. # else
  54. # # standard logger code
  55. # end
  56. # end
  57. # end
  58. # end
  59. #
  60. # Log subscriber also has some helpers to deal with logging and automatically
  61. # flushes all logs when the request finishes
  62. # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment.
  63. 1 class LogSubscriber < Subscriber
  64. # Embed in a String to clear all previous ANSI sequences.
  65. 1 CLEAR = "\e[0m"
  66. 1 BOLD = "\e[1m"
  67. # Colors
  68. 1 BLACK = "\e[30m"
  69. 1 RED = "\e[31m"
  70. 1 GREEN = "\e[32m"
  71. 1 YELLOW = "\e[33m"
  72. 1 BLUE = "\e[34m"
  73. 1 MAGENTA = "\e[35m"
  74. 1 CYAN = "\e[36m"
  75. 1 WHITE = "\e[37m"
  76. 1 mattr_accessor :colorize_logging, default: true
  77. 1 class << self
  78. 1 def logger
  79. @logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
  80. Rails.logger
  81. end
  82. end
  83. 1 attr_writer :logger
  84. 1 def log_subscribers
  85. subscribers
  86. end
  87. # Flush all log_subscribers' logger.
  88. 1 def flush_all!
  89. logger.flush if logger.respond_to?(:flush)
  90. end
  91. end
  92. 1 def logger
  93. LogSubscriber.logger
  94. end
  95. 1 def start(name, id, payload)
  96. super if logger
  97. end
  98. 1 def finish(name, id, payload)
  99. super if logger
  100. rescue => e
  101. if logger
  102. logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
  103. end
  104. end
  105. 1 private
  106. 1 %w(info debug warn error fatal unknown).each do |level|
  107. 6 class_eval <<-METHOD, __FILE__, __LINE__ + 1
  108. def #{level}(progname = nil, &block)
  109. logger.#{level}(progname, &block) if logger
  110. end
  111. METHOD
  112. end
  113. # Set color by using a symbol or one of the defined constants. If a third
  114. # option is set to +true+, it also adds bold to the string. This is based
  115. # on the Highline implementation and will automatically append CLEAR to the
  116. # end of the returned String.
  117. 1 def color(text, color, bold = false) # :doc:
  118. return text unless colorize_logging
  119. color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
  120. bold = bold ? BOLD : ""
  121. "#{bold}#{color}#{text}#{CLEAR}"
  122. end
  123. end
  124. end

lib/active_support/log_subscriber/test_helper.rb

52.63% lines covered

38 relevant lines. 20 lines covered and 18 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/log_subscriber"
  3. 1 require "active_support/logger"
  4. 1 require "active_support/notifications"
  5. 1 module ActiveSupport
  6. 1 class LogSubscriber
  7. # Provides some helpers to deal with testing log subscribers by setting up
  8. # notifications. Take for instance Active Record subscriber tests:
  9. #
  10. # class SyncLogSubscriberTest < ActiveSupport::TestCase
  11. # include ActiveSupport::LogSubscriber::TestHelper
  12. #
  13. # setup do
  14. # ActiveRecord::LogSubscriber.attach_to(:active_record)
  15. # end
  16. #
  17. # def test_basic_query_logging
  18. # Developer.all.to_a
  19. # wait
  20. # assert_equal 1, @logger.logged(:debug).size
  21. # assert_match(/Developer Load/, @logger.logged(:debug).last)
  22. # assert_match(/SELECT \* FROM "developers"/, @logger.logged(:debug).last)
  23. # end
  24. # end
  25. #
  26. # All you need to do is to ensure that your log subscriber is added to
  27. # Rails::Subscriber, as in the second line of the code above. The test
  28. # helpers are responsible for setting up the queue, subscriptions and
  29. # turning colors in logs off.
  30. #
  31. # The messages are available in the @logger instance, which is a logger with
  32. # limited powers (it actually does not send anything to your output), and
  33. # you can collect them doing @logger.logged(level), where level is the level
  34. # used in logging, like info, debug, warn and so on.
  35. 1 module TestHelper
  36. 1 def setup # :nodoc:
  37. @logger = MockLogger.new
  38. @notifier = ActiveSupport::Notifications::Fanout.new
  39. ActiveSupport::LogSubscriber.colorize_logging = false
  40. @old_notifier = ActiveSupport::Notifications.notifier
  41. set_logger(@logger)
  42. ActiveSupport::Notifications.notifier = @notifier
  43. end
  44. 1 def teardown # :nodoc:
  45. set_logger(nil)
  46. ActiveSupport::Notifications.notifier = @old_notifier
  47. end
  48. 1 class MockLogger
  49. 1 include ActiveSupport::Logger::Severity
  50. 1 attr_reader :flush_count
  51. 1 attr_accessor :level
  52. 1 def initialize(level = DEBUG)
  53. @flush_count = 0
  54. @level = level
  55. @logged = Hash.new { |h, k| h[k] = [] }
  56. end
  57. 1 def method_missing(level, message = nil)
  58. if block_given?
  59. @logged[level] << yield
  60. else
  61. @logged[level] << message
  62. end
  63. end
  64. 1 def logged(level)
  65. @logged[level].compact.map { |l| l.to_s.strip }
  66. end
  67. 1 def flush
  68. @flush_count += 1
  69. end
  70. 1 ActiveSupport::Logger::Severity.constants.each do |severity|
  71. 6 class_eval <<-EOT, __FILE__, __LINE__ + 1
  72. def #{severity.downcase}?
  73. #{severity} >= @level
  74. end
  75. EOT
  76. end
  77. end
  78. # Wait notifications to be published.
  79. 1 def wait
  80. @notifier.wait
  81. end
  82. # Overwrite if you use another logger in your log subscriber.
  83. #
  84. # def logger
  85. # ActiveRecord::Base.logger = @logger
  86. # end
  87. 1 def set_logger(logger)
  88. ActiveSupport::LogSubscriber.logger = logger
  89. end
  90. end
  91. end
  92. end

lib/active_support/logger.rb

22.92% lines covered

48 relevant lines. 11 lines covered and 37 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/logger_silence"
  3. 24 require "active_support/logger_thread_safe_level"
  4. 24 require "logger"
  5. 24 module ActiveSupport
  6. 24 class Logger < ::Logger
  7. 24 include LoggerSilence
  8. # Returns true if the logger destination matches one of the sources
  9. #
  10. # logger = Logger.new(STDOUT)
  11. # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
  12. # # => true
  13. 24 def self.logger_outputs_to?(logger, *sources)
  14. logdev = logger.instance_variable_get(:@logdev)
  15. logger_source = logdev.dev if logdev.respond_to?(:dev)
  16. sources.any? { |source| source == logger_source }
  17. end
  18. # Broadcasts logs to multiple loggers.
  19. 24 def self.broadcast(logger) # :nodoc:
  20. Module.new do
  21. define_method(:add) do |*args, &block|
  22. logger.add(*args, &block)
  23. super(*args, &block)
  24. end
  25. define_method(:<<) do |x|
  26. logger << x
  27. super(x)
  28. end
  29. define_method(:close) do
  30. logger.close
  31. super()
  32. end
  33. define_method(:progname=) do |name|
  34. logger.progname = name
  35. super(name)
  36. end
  37. define_method(:formatter=) do |formatter|
  38. logger.formatter = formatter
  39. super(formatter)
  40. end
  41. define_method(:level=) do |level|
  42. logger.level = level
  43. super(level)
  44. end
  45. define_method(:local_level=) do |level|
  46. logger.local_level = level if logger.respond_to?(:local_level=)
  47. super(level) if respond_to?(:local_level=)
  48. end
  49. define_method(:silence) do |level = Logger::ERROR, &block|
  50. if logger.respond_to?(:silence)
  51. logger.silence(level) do
  52. if defined?(super)
  53. super(level, &block)
  54. else
  55. block.call(self)
  56. end
  57. end
  58. else
  59. if defined?(super)
  60. super(level, &block)
  61. else
  62. block.call(self)
  63. end
  64. end
  65. end
  66. end
  67. end
  68. 24 def initialize(*args, **kwargs)
  69. super
  70. @formatter = SimpleFormatter.new
  71. end
  72. # Simple formatter which only displays the message.
  73. 24 class SimpleFormatter < ::Logger::Formatter
  74. # This method is invoked when a log event occurs
  75. 24 def call(severity, timestamp, progname, msg)
  76. "#{String === msg ? msg : msg.inspect}\n"
  77. end
  78. end
  79. end
  80. end

lib/active_support/logger_silence.rb

81.25% lines covered

16 relevant lines. 13 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/concern"
  3. 24 require "active_support/core_ext/module/attribute_accessors"
  4. 24 require "active_support/logger_thread_safe_level"
  5. 24 module LoggerSilence
  6. 24 extend ActiveSupport::Concern
  7. 24 included do
  8. ActiveSupport::Deprecation.warn(
  9. "Including LoggerSilence is deprecated and will be removed in Rails 6.1. " \
  10. "Please use `ActiveSupport::LoggerSilence` instead"
  11. )
  12. include ActiveSupport::LoggerSilence
  13. end
  14. end
  15. 24 module ActiveSupport
  16. 24 module LoggerSilence
  17. 24 extend ActiveSupport::Concern
  18. 24 included do
  19. 26 cattr_accessor :silencer, default: true
  20. 26 include ActiveSupport::LoggerThreadSafeLevel
  21. end
  22. # Silences the logger for the duration of the block.
  23. 24 def silence(severity = Logger::ERROR)
  24. silencer ? log_at(severity) { yield self } : yield(self)
  25. end
  26. end
  27. end

lib/active_support/logger_thread_safe_level.rb

46.15% lines covered

39 relevant lines. 18 lines covered and 21 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/concern"
  3. 24 require "active_support/core_ext/module/attribute_accessors"
  4. 24 require "concurrent"
  5. 24 require "fiber"
  6. 24 module ActiveSupport
  7. 24 module LoggerThreadSafeLevel # :nodoc:
  8. 24 extend ActiveSupport::Concern
  9. 24 included do
  10. 26 cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false
  11. end
  12. 24 Logger::Severity.constants.each do |severity|
  13. 144 class_eval(<<-EOT, __FILE__, __LINE__ + 1)
  14. def #{severity.downcase}? # def debug?
  15. Logger::#{severity} >= level # DEBUG >= level
  16. end # end
  17. EOT
  18. end
  19. 24 def after_initialize
  20. ActiveSupport::Deprecation.warn(
  21. "Logger don't need to call #after_initialize directly anymore. It will be deprecated without replacement in " \
  22. "Rails 6.1."
  23. )
  24. end
  25. 24 def local_log_id
  26. Fiber.current.__id__
  27. end
  28. 24 def local_level
  29. self.class.local_levels[local_log_id]
  30. end
  31. 24 def local_level=(level)
  32. case level
  33. when Integer
  34. self.class.local_levels[local_log_id] = level
  35. when Symbol
  36. self.class.local_levels[local_log_id] = Logger::Severity.const_get(level.to_s.upcase)
  37. when nil
  38. self.class.local_levels.delete(local_log_id)
  39. else
  40. raise ArgumentError, "Invalid log level: #{level.inspect}"
  41. end
  42. end
  43. 24 def level
  44. local_level || super
  45. end
  46. # Change the thread-local level for the duration of the given block.
  47. 24 def log_at(level)
  48. old_local_level, self.local_level = local_level, level
  49. yield
  50. ensure
  51. self.local_level = old_local_level
  52. end
  53. # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
  54. # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
  55. 24 def add(severity, message = nil, progname = nil, &block) #:nodoc:
  56. severity ||= UNKNOWN
  57. progname ||= @progname
  58. return true if @logdev.nil? || severity < level
  59. if message.nil?
  60. if block_given?
  61. message = yield
  62. else
  63. message = progname
  64. progname = @progname
  65. end
  66. end
  67. @logdev.write \
  68. format_message(format_severity(severity), Time.now, progname, message)
  69. end
  70. end
  71. end

lib/active_support/message_encryptor.rb

40.0% lines covered

75 relevant lines. 30 lines covered and 45 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "openssl"
  3. 2 require "base64"
  4. 2 require "active_support/core_ext/module/attribute_accessors"
  5. 2 require "active_support/message_verifier"
  6. 2 require "active_support/messages/metadata"
  7. 2 module ActiveSupport
  8. # MessageEncryptor is a simple way to encrypt values which get stored
  9. # somewhere you don't trust.
  10. #
  11. # The cipher text and initialization vector are base64 encoded and returned
  12. # to you.
  13. #
  14. # This can be used in situations similar to the <tt>MessageVerifier</tt>, but
  15. # where you don't want users to be able to determine the value of the payload.
  16. #
  17. # len = ActiveSupport::MessageEncryptor.key_len
  18. # salt = SecureRandom.random_bytes(len)
  19. # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\x89\xE0\x156\xAC..."
  20. # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
  21. # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
  22. # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
  23. #
  24. # === Confining messages to a specific purpose
  25. #
  26. # By default any message can be used throughout your app. But they can also be
  27. # confined to a specific +:purpose+.
  28. #
  29. # token = crypt.encrypt_and_sign("this is the chair", purpose: :login)
  30. #
  31. # Then that same purpose must be passed when verifying to get the data back out:
  32. #
  33. # crypt.decrypt_and_verify(token, purpose: :login) # => "this is the chair"
  34. # crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
  35. # crypt.decrypt_and_verify(token) # => nil
  36. #
  37. # Likewise, if a message has no purpose it won't be returned when verifying with
  38. # a specific purpose.
  39. #
  40. # token = crypt.encrypt_and_sign("the conversation is lively")
  41. # crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
  42. # crypt.decrypt_and_verify(token) # => "the conversation is lively"
  43. #
  44. # === Making messages expire
  45. #
  46. # By default messages last forever and verifying one year from now will still
  47. # return the original value. But messages can be set to expire at a given
  48. # time with +:expires_in+ or +:expires_at+.
  49. #
  50. # crypt.encrypt_and_sign(parcel, expires_in: 1.month)
  51. # crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)
  52. #
  53. # Then the messages can be verified and returned up to the expire time.
  54. # Thereafter, verifying returns +nil+.
  55. #
  56. # === Rotating keys
  57. #
  58. # MessageEncryptor also supports rotating out old configurations by falling
  59. # back to a stack of encryptors. Call +rotate+ to build and add an encryptor
  60. # so +decrypt_and_verify+ will also try the fallback.
  61. #
  62. # By default any rotated encryptors use the values of the primary
  63. # encryptor unless specified otherwise.
  64. #
  65. # You'd give your encryptor the new defaults:
  66. #
  67. # crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
  68. #
  69. # Then gradually rotate the old values out by adding them as fallbacks. Any message
  70. # generated with the old values will then work until the rotation is removed.
  71. #
  72. # crypt.rotate old_secret # Fallback to an old secret instead of @secret.
  73. # crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.
  74. #
  75. # Though if both the secret and the cipher was changed at the same time,
  76. # the above should be combined into:
  77. #
  78. # crypt.rotate old_secret, cipher: "aes-256-cbc"
  79. 2 class MessageEncryptor
  80. 2 prepend Messages::Rotator::Encryptor
  81. 2 cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
  82. 2 class << self
  83. 2 def default_cipher #:nodoc:
  84. if use_authenticated_message_encryption
  85. "aes-256-gcm"
  86. else
  87. "aes-256-cbc"
  88. end
  89. end
  90. end
  91. 2 module NullSerializer #:nodoc:
  92. 2 def self.load(value)
  93. value
  94. end
  95. 2 def self.dump(value)
  96. value
  97. end
  98. end
  99. 2 module NullVerifier #:nodoc:
  100. 2 def self.verify(value)
  101. value
  102. end
  103. 2 def self.generate(value)
  104. value
  105. end
  106. end
  107. 2 class InvalidMessage < StandardError; end
  108. 2 OpenSSLCipherError = OpenSSL::Cipher::CipherError
  109. # Initialize a new MessageEncryptor. +secret+ must be at least as long as
  110. # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
  111. # bits. If you are using a user-entered secret, you can generate a suitable
  112. # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
  113. # derivation function.
  114. #
  115. # First additional parameter is used as the signature key for +MessageVerifier+.
  116. # This allows you to specify keys to encrypt and sign data.
  117. #
  118. # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
  119. #
  120. # Options:
  121. # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
  122. # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
  123. # * <tt>:digest</tt> - String of digest to use for signing. Default is
  124. # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
  125. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
  126. 2 def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
  127. @secret = secret
  128. @sign_secret = sign_secret
  129. @cipher = cipher || self.class.default_cipher
  130. @digest = digest || "SHA1" unless aead_mode?
  131. @verifier = resolve_verifier
  132. @serializer = serializer || Marshal
  133. end
  134. # Encrypt and sign a message. We need to sign the message in order to avoid
  135. # padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
  136. 2 def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
  137. verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
  138. end
  139. # Decrypt and verify a message. We need to verify the message in order to
  140. # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
  141. 2 def decrypt_and_verify(data, purpose: nil, **)
  142. _decrypt(verifier.verify(data), purpose)
  143. end
  144. # Given a cipher, returns the key length of the cipher to help generate the key of desired size
  145. 2 def self.key_len(cipher = default_cipher)
  146. OpenSSL::Cipher.new(cipher).key_len
  147. end
  148. 2 private
  149. 2 def _encrypt(value, **metadata_options)
  150. cipher = new_cipher
  151. cipher.encrypt
  152. cipher.key = @secret
  153. # Rely on OpenSSL for the initialization vector
  154. iv = cipher.random_iv
  155. cipher.auth_data = "" if aead_mode?
  156. encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
  157. encrypted_data << cipher.final
  158. blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
  159. blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
  160. blob
  161. end
  162. 2 def _decrypt(encrypted_message, purpose)
  163. cipher = new_cipher
  164. encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
  165. # Currently the OpenSSL bindings do not raise an error if auth_tag is
  166. # truncated, which would allow an attacker to easily forge it. See
  167. # https://github.com/ruby/openssl/issues/63
  168. raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
  169. cipher.decrypt
  170. cipher.key = @secret
  171. cipher.iv = iv
  172. if aead_mode?
  173. cipher.auth_tag = auth_tag
  174. cipher.auth_data = ""
  175. end
  176. decrypted_data = cipher.update(encrypted_data)
  177. decrypted_data << cipher.final
  178. message = Messages::Metadata.verify(decrypted_data, purpose)
  179. @serializer.load(message) if message
  180. rescue OpenSSLCipherError, TypeError, ArgumentError
  181. raise InvalidMessage
  182. end
  183. 2 def new_cipher
  184. OpenSSL::Cipher.new(@cipher)
  185. end
  186. 2 attr_reader :verifier
  187. 2 def aead_mode?
  188. @aead_mode ||= new_cipher.authenticated?
  189. end
  190. 2 def resolve_verifier
  191. if aead_mode?
  192. NullVerifier
  193. else
  194. MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
  195. end
  196. end
  197. end
  198. end

lib/active_support/message_verifier.rb

45.0% lines covered

40 relevant lines. 18 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "base64"
  3. 2 require "active_support/core_ext/object/blank"
  4. 2 require "active_support/security_utils"
  5. 2 require "active_support/messages/metadata"
  6. 2 require "active_support/messages/rotator"
  7. 2 module ActiveSupport
  8. # +MessageVerifier+ makes it easy to generate and verify messages which are
  9. # signed to prevent tampering.
  10. #
  11. # This is useful for cases like remember-me tokens and auto-unsubscribe links
  12. # where the session store isn't suitable or available.
  13. #
  14. # Remember Me:
  15. # cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
  16. #
  17. # In the authentication filter:
  18. #
  19. # id, time = @verifier.verify(cookies[:remember_me])
  20. # if Time.now < time
  21. # self.current_user = User.find(id)
  22. # end
  23. #
  24. # By default it uses Marshal to serialize the message. If you want to use
  25. # another serialization method, you can set the serializer in the options
  26. # hash upon initialization:
  27. #
  28. # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
  29. #
  30. # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
  31. # If you want to use a different hash algorithm, you can change it by providing
  32. # +:digest+ key as an option while initializing the verifier:
  33. #
  34. # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
  35. #
  36. # === Confining messages to a specific purpose
  37. #
  38. # By default any message can be used throughout your app. But they can also be
  39. # confined to a specific +:purpose+.
  40. #
  41. # token = @verifier.generate("this is the chair", purpose: :login)
  42. #
  43. # Then that same purpose must be passed when verifying to get the data back out:
  44. #
  45. # @verifier.verified(token, purpose: :login) # => "this is the chair"
  46. # @verifier.verified(token, purpose: :shipping) # => nil
  47. # @verifier.verified(token) # => nil
  48. #
  49. # @verifier.verify(token, purpose: :login) # => "this is the chair"
  50. # @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
  51. # @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature
  52. #
  53. # Likewise, if a message has no purpose it won't be returned when verifying with
  54. # a specific purpose.
  55. #
  56. # token = @verifier.generate("the conversation is lively")
  57. # @verifier.verified(token, purpose: :scare_tactics) # => nil
  58. # @verifier.verified(token) # => "the conversation is lively"
  59. #
  60. # @verifier.verify(token, purpose: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature
  61. # @verifier.verify(token) # => "the conversation is lively"
  62. #
  63. # === Making messages expire
  64. #
  65. # By default messages last forever and verifying one year from now will still
  66. # return the original value. But messages can be set to expire at a given
  67. # time with +:expires_in+ or +:expires_at+.
  68. #
  69. # @verifier.generate(parcel, expires_in: 1.month)
  70. # @verifier.generate(doowad, expires_at: Time.now.end_of_year)
  71. #
  72. # Then the messages can be verified and returned up to the expire time.
  73. # Thereafter, the +verified+ method returns +nil+ while +verify+ raises
  74. # <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
  75. #
  76. # === Rotating keys
  77. #
  78. # MessageVerifier also supports rotating out old configurations by falling
  79. # back to a stack of verifiers. Call +rotate+ to build and add a verifier to
  80. # so either +verified+ or +verify+ will also try verifying with the fallback.
  81. #
  82. # By default any rotated verifiers use the values of the primary
  83. # verifier unless specified otherwise.
  84. #
  85. # You'd give your verifier the new defaults:
  86. #
  87. # verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)
  88. #
  89. # Then gradually rotate the old values out by adding them as fallbacks. Any message
  90. # generated with the old values will then work until the rotation is removed.
  91. #
  92. # verifier.rotate old_secret # Fallback to an old secret instead of @secret.
  93. # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
  94. # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
  95. #
  96. # Though the above would most likely be combined into one rotation:
  97. #
  98. # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal
  99. 2 class MessageVerifier
  100. 2 prepend Messages::Rotator::Verifier
  101. 2 class InvalidSignature < StandardError; end
  102. 2 def initialize(secret, digest: nil, serializer: nil)
  103. raise ArgumentError, "Secret should not be nil." unless secret
  104. @secret = secret
  105. @digest = digest || "SHA1"
  106. @serializer = serializer || Marshal
  107. end
  108. # Checks if a signed message could have been generated by signing an object
  109. # with the +MessageVerifier+'s secret.
  110. #
  111. # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
  112. # signed_message = verifier.generate 'a private message'
  113. # verifier.valid_message?(signed_message) # => true
  114. #
  115. # tampered_message = signed_message.chop # editing the message invalidates the signature
  116. # verifier.valid_message?(tampered_message) # => false
  117. 2 def valid_message?(signed_message)
  118. return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
  119. data, digest = signed_message.split("--")
  120. data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
  121. end
  122. # Decodes the signed message using the +MessageVerifier+'s secret.
  123. #
  124. # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
  125. #
  126. # signed_message = verifier.generate 'a private message'
  127. # verifier.verified(signed_message) # => 'a private message'
  128. #
  129. # Returns +nil+ if the message was not signed with the same secret.
  130. #
  131. # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
  132. # other_verifier.verified(signed_message) # => nil
  133. #
  134. # Returns +nil+ if the message is not Base64-encoded.
  135. #
  136. # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
  137. # verifier.verified(invalid_message) # => nil
  138. #
  139. # Raises any error raised while decoding the signed message.
  140. #
  141. # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
  142. # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
  143. 2 def verified(signed_message, purpose: nil, **)
  144. if valid_message?(signed_message)
  145. begin
  146. data = signed_message.split("--")[0]
  147. message = Messages::Metadata.verify(decode(data), purpose)
  148. @serializer.load(message) if message
  149. rescue ArgumentError => argument_error
  150. return if argument_error.message.include?("invalid base64")
  151. raise
  152. end
  153. end
  154. end
  155. # Decodes the signed message using the +MessageVerifier+'s secret.
  156. #
  157. # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
  158. # signed_message = verifier.generate 'a private message'
  159. #
  160. # verifier.verify(signed_message) # => 'a private message'
  161. #
  162. # Raises +InvalidSignature+ if the message was not signed with the same
  163. # secret or was not Base64-encoded.
  164. #
  165. # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
  166. # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
  167. 2 def verify(*args, **options)
  168. verified(*args, **options) || raise(InvalidSignature)
  169. end
  170. # Generates a signed message for the provided value.
  171. #
  172. # The message is signed with the +MessageVerifier+'s secret.
  173. # Returns Base64-encoded message joined with the generated signature.
  174. #
  175. # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
  176. # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
  177. 2 def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
  178. data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
  179. "#{data}--#{generate_digest(data)}"
  180. end
  181. 2 private
  182. 2 def encode(data)
  183. ::Base64.strict_encode64(data)
  184. end
  185. 2 def decode(data)
  186. ::Base64.strict_decode64(data)
  187. end
  188. 2 def generate_digest(data)
  189. require "openssl" unless defined?(OpenSSL)
  190. OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
  191. end
  192. end
  193. end

lib/active_support/messages/metadata.rb

47.37% lines covered

38 relevant lines. 18 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "time"
  3. 2 module ActiveSupport
  4. 2 module Messages #:nodoc:
  5. 2 class Metadata #:nodoc:
  6. 2 def initialize(message, expires_at = nil, purpose = nil)
  7. @message, @purpose = message, purpose
  8. @expires_at = expires_at.is_a?(String) ? Time.iso8601(expires_at) : expires_at
  9. end
  10. 2 def as_json(options = {})
  11. { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
  12. end
  13. 2 class << self
  14. 2 def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
  15. if expires_at || expires_in || purpose
  16. JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
  17. else
  18. message
  19. end
  20. end
  21. 2 def verify(message, purpose)
  22. extract_metadata(message).verify(purpose)
  23. end
  24. 2 private
  25. 2 def pick_expiry(expires_at, expires_in)
  26. if expires_at
  27. expires_at.utc.iso8601(3)
  28. elsif expires_in
  29. Time.now.utc.advance(seconds: expires_in).iso8601(3)
  30. end
  31. end
  32. 2 def extract_metadata(message)
  33. data = JSON.decode(message) rescue nil
  34. if data.is_a?(Hash) && data.key?("_rails")
  35. new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
  36. else
  37. new(message)
  38. end
  39. end
  40. 2 def encode(message)
  41. ::Base64.strict_encode64(message)
  42. end
  43. 2 def decode(message)
  44. ::Base64.strict_decode64(message)
  45. end
  46. end
  47. 2 def verify(purpose)
  48. @message if match?(purpose) && fresh?
  49. end
  50. 2 private
  51. 2 def match?(purpose)
  52. @purpose.to_s == purpose.to_s
  53. end
  54. 2 def fresh?
  55. @expires_at.nil? || Time.now.utc < @expires_at
  56. end
  57. end
  58. end
  59. end

lib/active_support/messages/rotation_configuration.rb

54.55% lines covered

11 relevant lines. 6 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module ActiveSupport
  3. 2 module Messages
  4. 2 class RotationConfiguration # :nodoc:
  5. 2 attr_reader :signed, :encrypted
  6. 2 def initialize
  7. @signed, @encrypted = [], []
  8. end
  9. 2 def rotate(kind, *args, **options)
  10. args << options unless options.empty?
  11. case kind
  12. when :signed
  13. @signed << args
  14. when :encrypted
  15. @encrypted << args
  16. end
  17. end
  18. end
  19. end
  20. end

lib/active_support/messages/rotator.rb

54.84% lines covered

31 relevant lines. 17 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module ActiveSupport
  3. 2 module Messages
  4. 2 module Rotator # :nodoc:
  5. 2 def initialize(*secrets, on_rotation: nil, **options)
  6. super(*secrets, **options)
  7. @options = options
  8. @rotations = []
  9. @on_rotation = on_rotation
  10. end
  11. 2 def rotate(*secrets, **options)
  12. @rotations << build_rotation(*secrets, @options.merge(options))
  13. end
  14. 2 module Encryptor
  15. 2 include Rotator
  16. 2 def decrypt_and_verify(*args, on_rotation: @on_rotation, **options)
  17. super
  18. rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
  19. run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
  20. end
  21. 2 private
  22. 2 def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
  23. self.class.new(secret, sign_secret, **options)
  24. end
  25. end
  26. 2 module Verifier
  27. 2 include Rotator
  28. 2 def verified(*args, on_rotation: @on_rotation, **options)
  29. super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
  30. end
  31. 2 private
  32. 2 def build_rotation(secret = @secret, options)
  33. self.class.new(secret, **options)
  34. end
  35. end
  36. 2 private
  37. 2 def run_rotations(on_rotation)
  38. @rotations.find do |rotation|
  39. if message = yield(rotation) rescue next
  40. on_rotation&.call
  41. return message
  42. end
  43. end
  44. end
  45. end
  46. end
  47. end

lib/active_support/multibyte.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport #:nodoc:
  3. 23 module Multibyte
  4. 23 autoload :Chars, "active_support/multibyte/chars"
  5. 23 autoload :Unicode, "active_support/multibyte/unicode"
  6. # The proxy class returned when calling mb_chars. You can use this accessor
  7. # to configure your own proxy class so you can support other encodings. See
  8. # the ActiveSupport::Multibyte::Chars implementation for an example how to
  9. # do this.
  10. #
  11. # ActiveSupport::Multibyte.proxy_class = CharsForUTF32
  12. 23 def self.proxy_class=(klass)
  13. @proxy_class = klass
  14. end
  15. # Returns the current proxy class.
  16. 23 def self.proxy_class
  17. @proxy_class ||= ActiveSupport::Multibyte::Chars
  18. end
  19. end
  20. end

lib/active_support/multibyte/chars.rb

0.0% lines covered

98 relevant lines. 0 lines covered and 98 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/json"
  3. require "active_support/core_ext/string/access"
  4. require "active_support/core_ext/string/behavior"
  5. require "active_support/core_ext/symbol/starts_ends_with"
  6. require "active_support/core_ext/module/delegation"
  7. module ActiveSupport #:nodoc:
  8. module Multibyte #:nodoc:
  9. # Chars enables you to work transparently with UTF-8 encoding in the Ruby
  10. # String class without having extensive knowledge about the encoding. A
  11. # Chars object accepts a string upon initialization and proxies String
  12. # methods in an encoding safe manner. All the normal String methods are also
  13. # implemented on the proxy.
  14. #
  15. # String methods are proxied through the Chars object, and can be accessed
  16. # through the +mb_chars+ method. Methods which would normally return a
  17. # String object now return a Chars object so methods can be chained.
  18. #
  19. # 'The Perfect String '.mb_chars.downcase.strip
  20. # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
  21. #
  22. # Chars objects are perfectly interchangeable with String objects as long as
  23. # no explicit class checks are made. If certain methods do explicitly check
  24. # the class, call +to_s+ before you pass chars objects to them.
  25. #
  26. # bad.explicit_checking_method 'T'.mb_chars.downcase.to_s
  27. #
  28. # The default Chars implementation assumes that the encoding of the string
  29. # is UTF-8, if you want to handle different encodings you can write your own
  30. # multibyte string handler and configure it through
  31. # ActiveSupport::Multibyte.proxy_class.
  32. #
  33. # class CharsForUTF32
  34. # def size
  35. # @wrapped_string.size / 4
  36. # end
  37. #
  38. # def self.accepts?(string)
  39. # string.length % 4 == 0
  40. # end
  41. # end
  42. #
  43. # ActiveSupport::Multibyte.proxy_class = CharsForUTF32
  44. class Chars
  45. include Comparable
  46. attr_reader :wrapped_string
  47. alias to_s wrapped_string
  48. alias to_str wrapped_string
  49. delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
  50. # Creates a new Chars instance by wrapping _string_.
  51. def initialize(string)
  52. @wrapped_string = string
  53. @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
  54. end
  55. # Forward all undefined methods to the wrapped string.
  56. def method_missing(method, *args, &block)
  57. result = @wrapped_string.__send__(method, *args, &block)
  58. if method.end_with?("!")
  59. self if result
  60. else
  61. result.kind_of?(String) ? chars(result) : result
  62. end
  63. end
  64. # Returns +true+ if _obj_ responds to the given method. Private methods
  65. # are included in the search only if the optional second parameter
  66. # evaluates to +true+.
  67. def respond_to_missing?(method, include_private)
  68. @wrapped_string.respond_to?(method, include_private)
  69. end
  70. # Returns +true+ when the proxy class can handle the string. Returns
  71. # +false+ otherwise.
  72. def self.consumes?(string)
  73. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  74. ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
  75. removed from Rails 6.1. Use string.is_utf8? instead.
  76. MSG
  77. string.encoding == Encoding::UTF_8
  78. end
  79. # Works just like <tt>String#split</tt>, with the exception that the items
  80. # in the resulting list are Chars instances instead of String. This makes
  81. # chaining methods easier.
  82. #
  83. # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
  84. def split(*args)
  85. @wrapped_string.split(*args).map { |i| self.class.new(i) }
  86. end
  87. # Works like <tt>String#slice!</tt>, but returns an instance of
  88. # Chars, or +nil+ if the string was not modified. The string will not be
  89. # modified if the range given is out of bounds
  90. #
  91. # string = 'Welcome'
  92. # string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c">
  93. # string # => 'Welome'
  94. # string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo">
  95. # string # => 'me'
  96. def slice!(*args)
  97. string_sliced = @wrapped_string.slice!(*args)
  98. if string_sliced
  99. chars(string_sliced)
  100. end
  101. end
  102. # Reverses all characters in the string.
  103. #
  104. # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
  105. def reverse
  106. chars(@wrapped_string.scan(/\X/).reverse.join)
  107. end
  108. # Limits the byte size of the string to a number of bytes without breaking
  109. # characters. Usable when the storage for a string is limited for some
  110. # reason.
  111. #
  112. # 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
  113. def limit(limit)
  114. chars(@wrapped_string.truncate_bytes(limit, omission: nil))
  115. end
  116. # Capitalizes the first letter of every word, when possible.
  117. #
  118. # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
  119. # "日本語".mb_chars.titleize.to_s # => "日本語"
  120. def titleize
  121. chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
  122. end
  123. alias_method :titlecase, :titleize
  124. # Returns the KC normalization of the string by default. NFKC is
  125. # considered the best normalization form for passing strings to databases
  126. # and validations.
  127. #
  128. # * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
  129. # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
  130. # ActiveSupport::Multibyte::Unicode.default_normalization_form
  131. def normalize(form = nil)
  132. form ||= Unicode.default_normalization_form
  133. # See https://www.unicode.org/reports/tr15, Table 1
  134. if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
  135. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  136. ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
  137. removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
  138. MSG
  139. send(:unicode_normalize, alias_form)
  140. else
  141. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  142. ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
  143. removed from Rails 6.1. Use #unicode_normalize instead.
  144. MSG
  145. raise ArgumentError, "#{form} is not a valid normalization variant", caller
  146. end
  147. end
  148. # Performs canonical decomposition on all the characters.
  149. #
  150. # 'é'.length # => 2
  151. # 'é'.mb_chars.decompose.to_s.length # => 3
  152. def decompose
  153. chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
  154. end
  155. # Performs composition on all the characters.
  156. #
  157. # 'é'.length # => 3
  158. # 'é'.mb_chars.compose.to_s.length # => 2
  159. def compose
  160. chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
  161. end
  162. # Returns the number of grapheme clusters in the string.
  163. #
  164. # 'क्षि'.mb_chars.length # => 4
  165. # 'क्षि'.mb_chars.grapheme_length # => 3
  166. def grapheme_length
  167. @wrapped_string.scan(/\X/).length
  168. end
  169. # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
  170. # resulting in a valid UTF-8 string.
  171. #
  172. # Passing +true+ will forcibly tidy all bytes, assuming that the string's
  173. # encoding is entirely CP1252 or ISO-8859-1.
  174. def tidy_bytes(force = false)
  175. chars(Unicode.tidy_bytes(@wrapped_string, force))
  176. end
  177. def as_json(options = nil) #:nodoc:
  178. to_s.as_json(options)
  179. end
  180. %w(reverse tidy_bytes).each do |method|
  181. define_method("#{method}!") do |*args|
  182. @wrapped_string = send(method, *args).to_s
  183. self
  184. end
  185. end
  186. private
  187. def chars(string)
  188. self.class.new(string)
  189. end
  190. end
  191. end
  192. end

lib/active_support/multibyte/unicode.rb

37.74% lines covered

53 relevant lines. 20 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module ActiveSupport
  3. 2 module Multibyte
  4. 2 module Unicode
  5. 2 extend self
  6. # A list of all available normalization forms.
  7. # See https://www.unicode.org/reports/tr15/tr15-29.html for more
  8. # information about normalization.
  9. 2 NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
  10. 2 NORMALIZATION_FORM_ALIASES = { # :nodoc:
  11. c: :nfc,
  12. d: :nfd,
  13. kc: :nfkc,
  14. kd: :nfkd
  15. }
  16. # The Unicode version that is supported by the implementation
  17. 2 UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
  18. # The default normalization used for operations that require
  19. # normalization. It can be set to any of the normalizations
  20. # in NORMALIZATION_FORMS.
  21. #
  22. # ActiveSupport::Multibyte::Unicode.default_normalization_form = :c
  23. 2 attr_accessor :default_normalization_form
  24. 2 @default_normalization_form = :kc
  25. # Unpack the string at grapheme boundaries. Returns a list of character
  26. # lists.
  27. #
  28. # Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
  29. # Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
  30. 2 def unpack_graphemes(string)
  31. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  32. ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be
  33. removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead.
  34. MSG
  35. string.scan(/\X/).map(&:codepoints)
  36. end
  37. # Reverse operation of unpack_graphemes.
  38. #
  39. # Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि'
  40. 2 def pack_graphemes(unpacked)
  41. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  42. ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be
  43. removed from Rails 6.1. Use array.flatten.pack("U*") instead.
  44. MSG
  45. unpacked.flatten.pack("U*")
  46. end
  47. # Decompose composed characters to the decomposed form.
  48. 2 def decompose(type, codepoints)
  49. if type == :compatibility
  50. codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
  51. else
  52. codepoints.pack("U*").unicode_normalize(:nfd).codepoints
  53. end
  54. end
  55. # Compose decomposed characters to the composed form.
  56. 2 def compose(codepoints)
  57. codepoints.pack("U*").unicode_normalize(:nfc).codepoints
  58. end
  59. # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
  60. 2 if !defined?(Rubinius)
  61. # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
  62. # resulting in a valid UTF-8 string.
  63. #
  64. # Passing +true+ will forcibly tidy all bytes, assuming that the string's
  65. # encoding is entirely CP1252 or ISO-8859-1.
  66. 2 def tidy_bytes(string, force = false)
  67. return string if string.empty? || string.ascii_only?
  68. return recode_windows1252_chars(string) if force
  69. string.scrub { |bad| recode_windows1252_chars(bad) }
  70. end
  71. else
  72. def tidy_bytes(string, force = false)
  73. return string if string.empty?
  74. return recode_windows1252_chars(string) if force
  75. # We can't transcode to the same format, so we choose a nearly-identical encoding.
  76. # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
  77. # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
  78. # before returning.
  79. reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
  80. source = string.dup
  81. out = "".force_encoding(Encoding::UTF_16LE)
  82. loop do
  83. reader.primitive_convert(source, out)
  84. _, _, _, error_bytes, _ = reader.primitive_errinfo
  85. break if error_bytes.nil?
  86. out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
  87. end
  88. reader.finish
  89. out.encode!(Encoding::UTF_8)
  90. end
  91. end
  92. # Returns the KC normalization of the string by default. NFKC is
  93. # considered the best normalization form for passing strings to databases
  94. # and validations.
  95. #
  96. # * <tt>string</tt> - The string to perform normalization on.
  97. # * <tt>form</tt> - The form you want to normalize in. Should be one of
  98. # the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>.
  99. # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
  100. 2 def normalize(string, form = nil)
  101. form ||= @default_normalization_form
  102. # See https://www.unicode.org/reports/tr15, Table 1
  103. if alias_form = NORMALIZATION_FORM_ALIASES[form]
  104. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  105. ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
  106. removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead.
  107. MSG
  108. string.unicode_normalize(alias_form)
  109. else
  110. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  111. ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
  112. removed from Rails 6.1. Use String#unicode_normalize instead.
  113. MSG
  114. raise ArgumentError, "#{form} is not a valid normalization variant", caller
  115. end
  116. end
  117. 2 %w(downcase upcase swapcase).each do |method|
  118. 6 define_method(method) do |string|
  119. ActiveSupport::Deprecation.warn(<<-MSG.squish)
  120. ActiveSupport::Multibyte::Unicode##{method} is deprecated and
  121. will be removed from Rails 6.1. Use String methods directly.
  122. MSG
  123. string.send(method)
  124. end
  125. end
  126. 2 private
  127. 2 def recode_windows1252_chars(string)
  128. string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
  129. end
  130. end
  131. end
  132. end

lib/active_support/notifications.rb

62.5% lines covered

32 relevant lines. 20 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/notifications/instrumenter"
  3. 24 require "active_support/notifications/fanout"
  4. 24 require "active_support/per_thread_registry"
  5. 24 module ActiveSupport
  6. # = Notifications
  7. #
  8. # <tt>ActiveSupport::Notifications</tt> provides an instrumentation API for
  9. # Ruby.
  10. #
  11. # == Instrumenters
  12. #
  13. # To instrument an event you just need to do:
  14. #
  15. # ActiveSupport::Notifications.instrument('render', extra: :information) do
  16. # render plain: 'Foo'
  17. # end
  18. #
  19. # That first executes the block and then notifies all subscribers once done.
  20. #
  21. # In the example above +render+ is the name of the event, and the rest is called
  22. # the _payload_. The payload is a mechanism that allows instrumenters to pass
  23. # extra information to subscribers. Payloads consist of a hash whose contents
  24. # are arbitrary and generally depend on the event.
  25. #
  26. # == Subscribers
  27. #
  28. # You can consume those events and the information they provide by registering
  29. # a subscriber.
  30. #
  31. # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
  32. # name # => String, name of the event (such as 'render' from above)
  33. # start # => Time, when the instrumented block started execution
  34. # finish # => Time, when the instrumented block ended execution
  35. # id # => String, unique ID for the instrumenter that fired the event
  36. # payload # => Hash, the payload
  37. # end
  38. #
  39. # Here, the +start+ and +finish+ values represent wall-clock time. If you are
  40. # concerned about accuracy, you can register a monotonic subscriber.
  41. #
  42. # ActiveSupport::Notifications.monotonic_subscribe('render') do |name, start, finish, id, payload|
  43. # name # => String, name of the event (such as 'render' from above)
  44. # start # => Monotonic time, when the instrumented block started execution
  45. # finish # => Monotonic time, when the instrumented block ended execution
  46. # id # => String, unique ID for the instrumenter that fired the event
  47. # payload # => Hash, the payload
  48. # end
  49. #
  50. # The +start+ and +finish+ values above represent monotonic time.
  51. #
  52. # For instance, let's store all "render" events in an array:
  53. #
  54. # events = []
  55. #
  56. # ActiveSupport::Notifications.subscribe('render') do |*args|
  57. # events << ActiveSupport::Notifications::Event.new(*args)
  58. # end
  59. #
  60. # That code returns right away, you are just subscribing to "render" events.
  61. # The block is saved and will be called whenever someone instruments "render":
  62. #
  63. # ActiveSupport::Notifications.instrument('render', extra: :information) do
  64. # render plain: 'Foo'
  65. # end
  66. #
  67. # event = events.first
  68. # event.name # => "render"
  69. # event.duration # => 10 (in milliseconds)
  70. # event.payload # => { extra: :information }
  71. #
  72. # The block in the <tt>subscribe</tt> call gets the name of the event, start
  73. # timestamp, end timestamp, a string with a unique identifier for that event's instrumenter
  74. # (something like "535801666f04d0298cd6"), and a hash with the payload, in
  75. # that order.
  76. #
  77. # If an exception happens during that particular instrumentation the payload will
  78. # have a key <tt>:exception</tt> with an array of two elements as value: a string with
  79. # the name of the exception class, and the exception message.
  80. # The <tt>:exception_object</tt> key of the payload will have the exception
  81. # itself as the value:
  82. #
  83. # event.payload[:exception] # => ["ArgumentError", "Invalid value"]
  84. # event.payload[:exception_object] # => #<ArgumentError: Invalid value>
  85. #
  86. # As the earlier example depicts, the class <tt>ActiveSupport::Notifications::Event</tt>
  87. # is able to take the arguments as they come and provide an object-oriented
  88. # interface to that data.
  89. #
  90. # It is also possible to pass an object which responds to <tt>call</tt> method
  91. # as the second parameter to the <tt>subscribe</tt> method instead of a block:
  92. #
  93. # module ActionController
  94. # class PageRequest
  95. # def call(name, started, finished, unique_id, payload)
  96. # Rails.logger.debug ['notification:', name, started, finished, unique_id, payload].join(' ')
  97. # end
  98. # end
  99. # end
  100. #
  101. # ActiveSupport::Notifications.subscribe('process_action.action_controller', ActionController::PageRequest.new)
  102. #
  103. # resulting in the following output within the logs including a hash with the payload:
  104. #
  105. # notification: process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {
  106. # controller: "Devise::SessionsController",
  107. # action: "new",
  108. # params: {"action"=>"new", "controller"=>"devise/sessions"},
  109. # format: :html,
  110. # method: "GET",
  111. # path: "/login/sign_in",
  112. # status: 200,
  113. # view_runtime: 279.3080806732178,
  114. # db_runtime: 40.053
  115. # }
  116. #
  117. # You can also subscribe to all events whose name matches a certain regexp:
  118. #
  119. # ActiveSupport::Notifications.subscribe(/render/) do |*args|
  120. # ...
  121. # end
  122. #
  123. # and even pass no argument to <tt>subscribe</tt>, in which case you are subscribing
  124. # to all events.
  125. #
  126. # == Temporary Subscriptions
  127. #
  128. # Sometimes you do not want to subscribe to an event for the entire life of
  129. # the application. There are two ways to unsubscribe.
  130. #
  131. # WARNING: The instrumentation framework is designed for long-running subscribers,
  132. # use this feature sparingly because it wipes some internal caches and that has
  133. # a negative impact on performance.
  134. #
  135. # === Subscribe While a Block Runs
  136. #
  137. # You can subscribe to some event temporarily while some block runs. For
  138. # example, in
  139. #
  140. # callback = lambda {|*args| ... }
  141. # ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
  142. # ...
  143. # end
  144. #
  145. # the callback will be called for all "sql.active_record" events instrumented
  146. # during the execution of the block. The callback is unsubscribed automatically
  147. # after that.
  148. #
  149. # To record +started+ and +finished+ values with monotonic time,
  150. # specify the optional <tt>:monotonic</tt> option to the
  151. # <tt>subscribed</tt> method. The <tt>:monotonic</tt> option is set
  152. # to +false+ by default.
  153. #
  154. # callback = lambda {|name, started, finished, unique_id, payload| ... }
  155. # ActiveSupport::Notifications.subscribed(callback, "sql.active_record", monotonic: true) do
  156. # ...
  157. # end
  158. #
  159. # === Manual Unsubscription
  160. #
  161. # The +subscribe+ method returns a subscriber object:
  162. #
  163. # subscriber = ActiveSupport::Notifications.subscribe("render") do |*args|
  164. # ...
  165. # end
  166. #
  167. # To prevent that block from being called anymore, just unsubscribe passing
  168. # that reference:
  169. #
  170. # ActiveSupport::Notifications.unsubscribe(subscriber)
  171. #
  172. # You can also unsubscribe by passing the name of the subscriber object. Note
  173. # that this will unsubscribe all subscriptions with the given name:
  174. #
  175. # ActiveSupport::Notifications.unsubscribe("render")
  176. #
  177. # Subscribers using a regexp or other pattern-matching object will remain subscribed
  178. # to all events that match their original pattern, unless those events match a string
  179. # passed to `unsubscribe`:
  180. #
  181. # subscriber = ActiveSupport::Notifications.subscribe(/render/) { }
  182. # ActiveSupport::Notifications.unsubscribe('render_template.action_view')
  183. # subscriber.matches?('render_template.action_view') # => false
  184. # subscriber.matches?('render_partial.action_view') # => true
  185. #
  186. # == Default Queue
  187. #
  188. # Notifications ships with a queue implementation that consumes and publishes events
  189. # to all log subscribers. You can use any queue implementation you want.
  190. #
  191. 24 module Notifications
  192. 24 class << self
  193. 24 attr_accessor :notifier
  194. 24 def publish(name, *args)
  195. notifier.publish(name, *args)
  196. end
  197. 24 def instrument(name, payload = {})
  198. if notifier.listening?(name)
  199. instrumenter.instrument(name, payload) { yield payload if block_given? }
  200. else
  201. yield payload if block_given?
  202. end
  203. end
  204. # Subscribe to a given event name with the passed +block+.
  205. #
  206. # You can subscribe to events by passing a String to match exact event
  207. # names, or by passing a Regexp to match all events that match a pattern.
  208. #
  209. # ActiveSupport::Notifications.subscribe(/render/) do |*args|
  210. # @event = ActiveSupport::Notifications::Event.new(*args)
  211. # end
  212. #
  213. # The +block+ will receive five parameters with information about the event:
  214. #
  215. # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
  216. # name # => String, name of the event (such as 'render' from above)
  217. # start # => Time, when the instrumented block started execution
  218. # finish # => Time, when the instrumented block ended execution
  219. # id # => String, unique ID for the instrumenter that fired the event
  220. # payload # => Hash, the payload
  221. # end
  222. #
  223. # If the block passed to the method only takes one parameter,
  224. # it will yield an event object to the block:
  225. #
  226. # ActiveSupport::Notifications.subscribe(/render/) do |event|
  227. # @event = event
  228. # end
  229. 24 def subscribe(pattern = nil, callback = nil, &block)
  230. 2 notifier.subscribe(pattern, callback, monotonic: false, &block)
  231. end
  232. 24 def monotonic_subscribe(pattern = nil, callback = nil, &block)
  233. notifier.subscribe(pattern, callback, monotonic: true, &block)
  234. end
  235. 24 def subscribed(callback, pattern = nil, monotonic: false, &block)
  236. subscriber = notifier.subscribe(pattern, callback, monotonic: monotonic)
  237. yield
  238. ensure
  239. unsubscribe(subscriber)
  240. end
  241. 24 def unsubscribe(subscriber_or_name)
  242. notifier.unsubscribe(subscriber_or_name)
  243. end
  244. 24 def instrumenter
  245. InstrumentationRegistry.instance.instrumenter_for(notifier)
  246. end
  247. end
  248. # This class is a registry which holds all of the +Instrumenter+ objects
  249. # in a particular thread local. To access the +Instrumenter+ object for a
  250. # particular +notifier+, you can call the following method:
  251. #
  252. # InstrumentationRegistry.instrumenter_for(notifier)
  253. #
  254. # The instrumenters for multiple notifiers are held in a single instance of
  255. # this class.
  256. 24 class InstrumentationRegistry # :nodoc:
  257. 24 extend ActiveSupport::PerThreadRegistry
  258. 24 def initialize
  259. @registry = {}
  260. end
  261. 24 def instrumenter_for(notifier)
  262. @registry[notifier] ||= Instrumenter.new(notifier)
  263. end
  264. end
  265. 24 self.notifier = Fanout.new
  266. end
  267. end

lib/active_support/notifications/fanout.rb

53.9% lines covered

141 relevant lines. 76 lines covered and 65 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "mutex_m"
  3. 24 require "concurrent/map"
  4. 24 require "set"
  5. 24 require "active_support/core_ext/object/try"
  6. 24 module ActiveSupport
  7. 24 module Notifications
  8. # This is a default queue implementation that ships with Notifications.
  9. # It just pushes events to all registered log subscribers.
  10. #
  11. # This class is thread safe. All methods are reentrant.
  12. 24 class Fanout
  13. 24 include Mutex_m
  14. 24 def initialize
  15. 26 @string_subscribers = Hash.new { |h, k| h[k] = [] }
  16. 24 @other_subscribers = []
  17. 24 @listeners_for = Concurrent::Map.new
  18. 24 super
  19. end
  20. 24 def subscribe(pattern = nil, callable = nil, monotonic: false, &block)
  21. 2 subscriber = Subscribers.new(pattern, callable || block, monotonic)
  22. 2 synchronize do
  23. 2 if String === pattern
  24. 2 @string_subscribers[pattern] << subscriber
  25. 2 @listeners_for.delete(pattern)
  26. else
  27. @other_subscribers << subscriber
  28. @listeners_for.clear
  29. end
  30. end
  31. 2 subscriber
  32. end
  33. 24 def unsubscribe(subscriber_or_name)
  34. synchronize do
  35. case subscriber_or_name
  36. when String
  37. @string_subscribers[subscriber_or_name].clear
  38. @listeners_for.delete(subscriber_or_name)
  39. @other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) }
  40. else
  41. pattern = subscriber_or_name.try(:pattern)
  42. if String === pattern
  43. @string_subscribers[pattern].delete(subscriber_or_name)
  44. @listeners_for.delete(pattern)
  45. else
  46. @other_subscribers.delete(subscriber_or_name)
  47. @listeners_for.clear
  48. end
  49. end
  50. end
  51. end
  52. 24 def start(name, id, payload)
  53. listeners_for(name).each { |s| s.start(name, id, payload) }
  54. end
  55. 24 def finish(name, id, payload, listeners = listeners_for(name))
  56. listeners.each { |s| s.finish(name, id, payload) }
  57. end
  58. 24 def publish(name, *args)
  59. listeners_for(name).each { |s| s.publish(name, *args) }
  60. end
  61. 24 def listeners_for(name)
  62. # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics)
  63. @listeners_for[name] || synchronize do
  64. # use synchronisation when accessing @subscribers
  65. @listeners_for[name] ||=
  66. @string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) }
  67. end
  68. end
  69. 24 def listening?(name)
  70. listeners_for(name).any?
  71. end
  72. # This is a sync queue, so there is no waiting.
  73. 24 def wait
  74. end
  75. 24 module Subscribers # :nodoc:
  76. 24 def self.new(pattern, listener, monotonic)
  77. 2 subscriber_class = monotonic ? MonotonicTimed : Timed
  78. 2 if listener.respond_to?(:start) && listener.respond_to?(:finish)
  79. 2 subscriber_class = Evented
  80. else
  81. # Doing all this to detect a block like `proc { |x| }` vs
  82. # `proc { |*x| }` or `proc { |**x| }`
  83. if listener.respond_to?(:parameters)
  84. params = listener.parameters
  85. if params.length == 1 && params.first.first == :opt
  86. subscriber_class = EventObject
  87. end
  88. end
  89. end
  90. 2 wrap_all pattern, subscriber_class.new(pattern, listener)
  91. end
  92. 24 def self.wrap_all(pattern, subscriber)
  93. 2 unless pattern
  94. AllMessages.new(subscriber)
  95. else
  96. 2 subscriber
  97. end
  98. end
  99. 24 class Matcher #:nodoc:
  100. 24 attr_reader :pattern, :exclusions
  101. 24 def self.wrap(pattern)
  102. 2 return pattern if String === pattern
  103. new(pattern)
  104. end
  105. 24 def initialize(pattern)
  106. @pattern = pattern
  107. @exclusions = Set.new
  108. end
  109. 24 def unsubscribe!(name)
  110. exclusions << -name if pattern === name
  111. end
  112. 24 def ===(name)
  113. pattern === name && !exclusions.include?(name)
  114. end
  115. end
  116. 24 class Evented #:nodoc:
  117. 24 attr_reader :pattern
  118. 24 def initialize(pattern, delegate)
  119. 2 @pattern = Matcher.wrap(pattern)
  120. 2 @delegate = delegate
  121. 2 @can_publish = delegate.respond_to?(:publish)
  122. end
  123. 24 def publish(name, *args)
  124. if @can_publish
  125. @delegate.publish name, *args
  126. end
  127. end
  128. 24 def start(name, id, payload)
  129. @delegate.start name, id, payload
  130. end
  131. 24 def finish(name, id, payload)
  132. @delegate.finish name, id, payload
  133. end
  134. 24 def subscribed_to?(name)
  135. pattern === name
  136. end
  137. 24 def matches?(name)
  138. pattern && pattern === name
  139. end
  140. 24 def unsubscribe!(name)
  141. pattern.unsubscribe!(name)
  142. end
  143. end
  144. 24 class Timed < Evented # :nodoc:
  145. 24 def publish(name, *args)
  146. @delegate.call name, *args
  147. end
  148. 24 def start(name, id, payload)
  149. timestack = Thread.current[:_timestack] ||= []
  150. timestack.push Time.now
  151. end
  152. 24 def finish(name, id, payload)
  153. timestack = Thread.current[:_timestack]
  154. started = timestack.pop
  155. @delegate.call(name, started, Time.now, id, payload)
  156. end
  157. end
  158. 24 class MonotonicTimed < Evented # :nodoc:
  159. 24 def publish(name, *args)
  160. @delegate.call name, *args
  161. end
  162. 24 def start(name, id, payload)
  163. timestack = Thread.current[:_timestack_monotonic] ||= []
  164. timestack.push Concurrent.monotonic_time
  165. end
  166. 24 def finish(name, id, payload)
  167. timestack = Thread.current[:_timestack_monotonic]
  168. started = timestack.pop
  169. @delegate.call(name, started, Concurrent.monotonic_time, id, payload)
  170. end
  171. end
  172. 24 class EventObject < Evented
  173. 24 def start(name, id, payload)
  174. stack = Thread.current[:_event_stack] ||= []
  175. event = build_event name, id, payload
  176. event.start!
  177. stack.push event
  178. end
  179. 24 def finish(name, id, payload)
  180. stack = Thread.current[:_event_stack]
  181. event = stack.pop
  182. event.payload = payload
  183. event.finish!
  184. @delegate.call event
  185. end
  186. 24 private
  187. 24 def build_event(name, id, payload)
  188. ActiveSupport::Notifications::Event.new name, nil, nil, id, payload
  189. end
  190. end
  191. 24 class AllMessages # :nodoc:
  192. 24 def initialize(delegate)
  193. @delegate = delegate
  194. end
  195. 24 def start(name, id, payload)
  196. @delegate.start name, id, payload
  197. end
  198. 24 def finish(name, id, payload)
  199. @delegate.finish name, id, payload
  200. end
  201. 24 def publish(name, *args)
  202. @delegate.publish name, *args
  203. end
  204. 24 def subscribed_to?(name)
  205. true
  206. end
  207. 24 def unsubscribe!(*)
  208. false
  209. end
  210. 24 alias :matches? :===
  211. end
  212. end
  213. end
  214. end
  215. end

lib/active_support/notifications/instrumenter.rb

43.04% lines covered

79 relevant lines. 34 lines covered and 45 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "securerandom"
  3. 24 module ActiveSupport
  4. 24 module Notifications
  5. # Instrumenters are stored in a thread local.
  6. 24 class Instrumenter
  7. 24 attr_reader :id
  8. 24 def initialize(notifier)
  9. @id = unique_id
  10. @notifier = notifier
  11. end
  12. # Given a block, instrument it by measuring the time taken to execute
  13. # and publish it. Without a block, simply send a message via the
  14. # notifier. Notice that events get sent even if an error occurs in the
  15. # passed-in block.
  16. 24 def instrument(name, payload = {})
  17. # some of the listeners might have state
  18. listeners_state = start name, payload
  19. begin
  20. yield payload if block_given?
  21. rescue Exception => e
  22. payload[:exception] = [e.class.name, e.message]
  23. payload[:exception_object] = e
  24. raise e
  25. ensure
  26. finish_with_state listeners_state, name, payload
  27. end
  28. end
  29. # Send a start notification with +name+ and +payload+.
  30. 24 def start(name, payload)
  31. @notifier.start name, @id, payload
  32. end
  33. # Send a finish notification with +name+ and +payload+.
  34. 24 def finish(name, payload)
  35. @notifier.finish name, @id, payload
  36. end
  37. 24 def finish_with_state(listeners_state, name, payload)
  38. @notifier.finish name, @id, payload, listeners_state
  39. end
  40. 24 private
  41. 24 def unique_id
  42. SecureRandom.hex(10)
  43. end
  44. end
  45. 24 class Event
  46. 24 attr_reader :name, :time, :end, :transaction_id, :children
  47. 24 attr_accessor :payload
  48. 24 def self.clock_gettime_supported? # :nodoc:
  49. 24 defined?(Process::CLOCK_THREAD_CPUTIME_ID) &&
  50. !Gem.win_platform? &&
  51. !RUBY_PLATFORM.match?(/solaris/i)
  52. end
  53. 24 private_class_method :clock_gettime_supported?
  54. 24 def initialize(name, start, ending, transaction_id, payload)
  55. @name = name
  56. @payload = payload.dup
  57. @time = start
  58. @transaction_id = transaction_id
  59. @end = ending
  60. @children = []
  61. @cpu_time_start = 0
  62. @cpu_time_finish = 0
  63. @allocation_count_start = 0
  64. @allocation_count_finish = 0
  65. end
  66. # Record information at the time this event starts
  67. 24 def start!
  68. @time = now
  69. @cpu_time_start = now_cpu
  70. @allocation_count_start = now_allocations
  71. end
  72. # Record information at the time this event finishes
  73. 24 def finish!
  74. @cpu_time_finish = now_cpu
  75. @end = now
  76. @allocation_count_finish = now_allocations
  77. end
  78. 24 def end=(ending)
  79. ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!)
  80. @end = ending
  81. end
  82. # Returns the CPU time (in milliseconds) passed since the call to
  83. # +start!+ and the call to +finish!+
  84. 24 def cpu_time
  85. (@cpu_time_finish - @cpu_time_start) * 1000
  86. end
  87. # Returns the idle time time (in milliseconds) passed since the call to
  88. # +start!+ and the call to +finish!+
  89. 24 def idle_time
  90. duration - cpu_time
  91. end
  92. # Returns the number of allocations made since the call to +start!+ and
  93. # the call to +finish!+
  94. 24 def allocations
  95. @allocation_count_finish - @allocation_count_start
  96. end
  97. # Returns the difference in milliseconds between when the execution of the
  98. # event started and when it ended.
  99. #
  100. # ActiveSupport::Notifications.subscribe('wait') do |*args|
  101. # @event = ActiveSupport::Notifications::Event.new(*args)
  102. # end
  103. #
  104. # ActiveSupport::Notifications.instrument('wait') do
  105. # sleep 1
  106. # end
  107. #
  108. # @event.duration # => 1000.138
  109. 24 def duration
  110. 1000.0 * (self.end - time)
  111. end
  112. 24 def <<(event)
  113. @children << event
  114. end
  115. 24 def parent_of?(event)
  116. @children.include? event
  117. end
  118. 24 private
  119. 24 def now
  120. Concurrent.monotonic_time
  121. end
  122. 24 if clock_gettime_supported?
  123. 24 def now_cpu
  124. Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
  125. end
  126. else
  127. def now_cpu
  128. 0
  129. end
  130. end
  131. 24 if defined?(JRUBY_VERSION)
  132. def now_allocations
  133. 0
  134. end
  135. else
  136. 24 def now_allocations
  137. GC.stat :total_allocated_objects
  138. end
  139. end
  140. end
  141. end
  142. end

lib/active_support/number_helper.rb

75.0% lines covered

28 relevant lines. 21 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport
  3. 1 module NumberHelper
  4. 1 extend ActiveSupport::Autoload
  5. 1 eager_autoload do
  6. 1 autoload :NumberConverter
  7. 1 autoload :RoundingHelper
  8. 1 autoload :NumberToRoundedConverter
  9. 1 autoload :NumberToDelimitedConverter
  10. 1 autoload :NumberToHumanConverter
  11. 1 autoload :NumberToHumanSizeConverter
  12. 1 autoload :NumberToPhoneConverter
  13. 1 autoload :NumberToCurrencyConverter
  14. 1 autoload :NumberToPercentageConverter
  15. end
  16. 1 extend self
  17. # Formats a +number+ into a phone number (US by default e.g., (555)
  18. # 123-9876). You can customize the format in the +options+ hash.
  19. #
  20. # ==== Options
  21. #
  22. # * <tt>:area_code</tt> - Adds parentheses around the area code.
  23. # * <tt>:delimiter</tt> - Specifies the delimiter to use
  24. # (defaults to "-").
  25. # * <tt>:extension</tt> - Specifies an extension to add to the
  26. # end of the generated number.
  27. # * <tt>:country_code</tt> - Sets the country code for the phone
  28. # number.
  29. # * <tt>:pattern</tt> - Specifies how the number is divided into three
  30. # groups with the custom regexp to override the default format.
  31. # ==== Examples
  32. #
  33. # number_to_phone(5551234) # => "555-1234"
  34. # number_to_phone('5551234') # => "555-1234"
  35. # number_to_phone(1235551234) # => "123-555-1234"
  36. # number_to_phone(1235551234, area_code: true) # => "(123) 555-1234"
  37. # number_to_phone(1235551234, delimiter: ' ') # => "123 555 1234"
  38. # number_to_phone(1235551234, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
  39. # number_to_phone(1235551234, country_code: 1) # => "+1-123-555-1234"
  40. # number_to_phone('123a456') # => "123a456"
  41. #
  42. # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
  43. # # => "+1.123.555.1234 x 1343"
  44. #
  45. # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
  46. # # => "(755) 6123-4567"
  47. # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
  48. # # => "133-1234-5678"
  49. 1 def number_to_phone(number, options = {})
  50. NumberToPhoneConverter.convert(number, options)
  51. end
  52. # Formats a +number+ into a currency string (e.g., $13.65). You
  53. # can customize the format in the +options+ hash.
  54. #
  55. # The currency unit and number formatting of the current locale will be used
  56. # unless otherwise specified in the provided options. No currency conversion
  57. # is performed. If the user is given a way to change their locale, they will
  58. # also be able to change the relative value of the currency displayed with
  59. # this helper. If your application will ever support multiple locales, you
  60. # may want to specify a constant <tt>:locale</tt> option or consider
  61. # using a library capable of currency conversion.
  62. #
  63. # ==== Options
  64. #
  65. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  66. # (defaults to current locale).
  67. # * <tt>:precision</tt> - Sets the level of precision (defaults
  68. # to 2).
  69. # * <tt>:round_mode</tt> - Determine how rounding is performed
  70. # (defaults to :default. See BigDecimal::mode)
  71. # * <tt>:unit</tt> - Sets the denomination of the currency
  72. # (defaults to "$").
  73. # * <tt>:separator</tt> - Sets the separator between the units
  74. # (defaults to ".").
  75. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  76. # to ",").
  77. # * <tt>:format</tt> - Sets the format for non-negative numbers
  78. # (defaults to "%u%n"). Fields are <tt>%u</tt> for the
  79. # currency, and <tt>%n</tt> for the number.
  80. # * <tt>:negative_format</tt> - Sets the format for negative
  81. # numbers (defaults to prepending a hyphen to the formatted
  82. # number given by <tt>:format</tt>). Accepts the same fields
  83. # than <tt>:format</tt>, except <tt>%n</tt> is here the
  84. # absolute value of the number.
  85. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  86. # insignificant zeros after the decimal separator (defaults to
  87. # +false+).
  88. #
  89. # ==== Examples
  90. #
  91. # number_to_currency(1234567890.50) # => "$1,234,567,890.50"
  92. # number_to_currency(1234567890.506) # => "$1,234,567,890.51"
  93. # number_to_currency(1234567890.506, precision: 3) # => "$1,234,567,890.506"
  94. # number_to_currency(1234567890.506, locale: :fr) # => "1 234 567 890,51 €"
  95. # number_to_currency('123a456') # => "$123a456"
  96. #
  97. # number_to_currency("123a456", raise: true) # => InvalidNumberError
  98. #
  99. # number_to_currency(-0.456789, precision: 0)
  100. # # => "$0"
  101. # number_to_currency(-1234567890.50, negative_format: '(%u%n)')
  102. # # => "($1,234,567,890.50)"
  103. # number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '')
  104. # # => "&pound;1234567890,50"
  105. # number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
  106. # # => "1234567890,50 &pound;"
  107. # number_to_currency(1234567890.50, strip_insignificant_zeros: true)
  108. # # => "$1,234,567,890.5"
  109. # number_to_currency(1234567890.50, precision: 0, round_mode: :up)
  110. # # => "$1,234,567,891"
  111. 1 def number_to_currency(number, options = {})
  112. NumberToCurrencyConverter.convert(number, options)
  113. end
  114. # Formats a +number+ as a percentage string (e.g., 65%). You can
  115. # customize the format in the +options+ hash.
  116. #
  117. # ==== Options
  118. #
  119. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  120. # (defaults to current locale).
  121. # * <tt>:precision</tt> - Sets the precision of the number
  122. # (defaults to 3). Keeps the number's precision if +nil+.
  123. # * <tt>:round_mode</tt> - Determine how rounding is performed
  124. # (defaults to :default. See BigDecimal::mode)
  125. # * <tt>:significant</tt> - If +true+, precision will be the number
  126. # of significant_digits. If +false+, the number of fractional
  127. # digits (defaults to +false+).
  128. # * <tt>:separator</tt> - Sets the separator between the
  129. # fractional and integer digits (defaults to ".").
  130. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  131. # to "").
  132. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  133. # insignificant zeros after the decimal separator (defaults to
  134. # +false+).
  135. # * <tt>:format</tt> - Specifies the format of the percentage
  136. # string The number field is <tt>%n</tt> (defaults to "%n%").
  137. #
  138. # ==== Examples
  139. #
  140. # number_to_percentage(100) # => "100.000%"
  141. # number_to_percentage('98') # => "98.000%"
  142. # number_to_percentage(100, precision: 0) # => "100%"
  143. # number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
  144. # number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
  145. # number_to_percentage(1000, locale: :fr) # => "1000,000%"
  146. # number_to_percentage(1000, precision: nil) # => "1000%"
  147. # number_to_percentage('98a') # => "98a%"
  148. # number_to_percentage(100, format: '%n %') # => "100.000 %"
  149. # number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
  150. 1 def number_to_percentage(number, options = {})
  151. NumberToPercentageConverter.convert(number, options)
  152. end
  153. # Formats a +number+ with grouped thousands using +delimiter+
  154. # (e.g., 12,324). You can customize the format in the +options+
  155. # hash.
  156. #
  157. # ==== Options
  158. #
  159. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  160. # (defaults to current locale).
  161. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  162. # to ",").
  163. # * <tt>:separator</tt> - Sets the separator between the
  164. # fractional and integer digits (defaults to ".").
  165. # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for
  166. # deriving the placement of delimiter. Helpful when using currency formats
  167. # like INR.
  168. #
  169. # ==== Examples
  170. #
  171. # number_to_delimited(12345678) # => "12,345,678"
  172. # number_to_delimited('123456') # => "123,456"
  173. # number_to_delimited(12345678.05) # => "12,345,678.05"
  174. # number_to_delimited(12345678, delimiter: '.') # => "12.345.678"
  175. # number_to_delimited(12345678, delimiter: ',') # => "12,345,678"
  176. # number_to_delimited(12345678.05, separator: ' ') # => "12,345,678 05"
  177. # number_to_delimited(12345678.05, locale: :fr) # => "12 345 678,05"
  178. # number_to_delimited('112a') # => "112a"
  179. # number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
  180. # # => "98 765 432,98"
  181. # number_to_delimited("123456.78",
  182. # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/)
  183. # # => "1,23,456.78"
  184. 1 def number_to_delimited(number, options = {})
  185. NumberToDelimitedConverter.convert(number, options)
  186. end
  187. # Formats a +number+ with the specified level of
  188. # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
  189. # +:significant+ is +false+, and 5 if +:significant+ is +true+).
  190. # You can customize the format in the +options+ hash.
  191. #
  192. # ==== Options
  193. #
  194. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  195. # (defaults to current locale).
  196. # * <tt>:precision</tt> - Sets the precision of the number
  197. # (defaults to 3). Keeps the number's precision if +nil+.
  198. # * <tt>:round_mode</tt> - Determine how rounding is performed
  199. # (defaults to :default. See BigDecimal::mode)
  200. # * <tt>:significant</tt> - If +true+, precision will be the number
  201. # of significant_digits. If +false+, the number of fractional
  202. # digits (defaults to +false+).
  203. # * <tt>:separator</tt> - Sets the separator between the
  204. # fractional and integer digits (defaults to ".").
  205. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  206. # to "").
  207. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  208. # insignificant zeros after the decimal separator (defaults to
  209. # +false+).
  210. #
  211. # ==== Examples
  212. #
  213. # number_to_rounded(111.2345) # => "111.235"
  214. # number_to_rounded(111.2345, precision: 2) # => "111.23"
  215. # number_to_rounded(13, precision: 5) # => "13.00000"
  216. # number_to_rounded(389.32314, precision: 0) # => "389"
  217. # number_to_rounded(111.2345, significant: true) # => "111"
  218. # number_to_rounded(111.2345, precision: 1, significant: true) # => "100"
  219. # number_to_rounded(13, precision: 5, significant: true) # => "13.000"
  220. # number_to_rounded(13, precision: nil) # => "13"
  221. # number_to_rounded(389.32314, precision: 0, round_mode: :up) # => "390"
  222. # number_to_rounded(111.234, locale: :fr) # => "111,234"
  223. #
  224. # number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
  225. # # => "13"
  226. #
  227. # number_to_rounded(389.32314, precision: 4, significant: true) # => "389.3"
  228. # number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
  229. # # => "1.111,23"
  230. 1 def number_to_rounded(number, options = {})
  231. NumberToRoundedConverter.convert(number, options)
  232. end
  233. # Formats the bytes in +number+ into a more understandable
  234. # representation (e.g., giving it 1500 yields 1.46 KB). This
  235. # method is useful for reporting file sizes to users. You can
  236. # customize the format in the +options+ hash.
  237. #
  238. # See <tt>number_to_human</tt> if you want to pretty-print a
  239. # generic number.
  240. #
  241. # ==== Options
  242. #
  243. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  244. # (defaults to current locale).
  245. # * <tt>:precision</tt> - Sets the precision of the number
  246. # (defaults to 3).
  247. # * <tt>:round_mode</tt> - Determine how rounding is performed
  248. # (defaults to :default. See BigDecimal::mode)
  249. # * <tt>:significant</tt> - If +true+, precision will be the number
  250. # of significant_digits. If +false+, the number of fractional
  251. # digits (defaults to +true+)
  252. # * <tt>:separator</tt> - Sets the separator between the
  253. # fractional and integer digits (defaults to ".").
  254. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  255. # to "").
  256. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  257. # insignificant zeros after the decimal separator (defaults to
  258. # +true+)
  259. #
  260. # ==== Examples
  261. #
  262. # number_to_human_size(123) # => "123 Bytes"
  263. # number_to_human_size(1234) # => "1.21 KB"
  264. # number_to_human_size(12345) # => "12.1 KB"
  265. # number_to_human_size(1234567) # => "1.18 MB"
  266. # number_to_human_size(1234567890) # => "1.15 GB"
  267. # number_to_human_size(1234567890123) # => "1.12 TB"
  268. # number_to_human_size(1234567890123456) # => "1.1 PB"
  269. # number_to_human_size(1234567890123456789) # => "1.07 EB"
  270. # number_to_human_size(1234567, precision: 2) # => "1.2 MB"
  271. # number_to_human_size(483989, precision: 2) # => "470 KB"
  272. # number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
  273. # number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB"
  274. # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
  275. # number_to_human_size(524288000, precision: 5) # => "500 MB"
  276. 1 def number_to_human_size(number, options = {})
  277. NumberToHumanSizeConverter.convert(number, options)
  278. end
  279. # Pretty prints (formats and approximates) a number in a way it
  280. # is more readable by humans (e.g.: 1200000000 becomes "1.2
  281. # Billion"). This is useful for numbers that can get very large
  282. # (and too hard to read).
  283. #
  284. # See <tt>number_to_human_size</tt> if you want to print a file
  285. # size.
  286. #
  287. # You can also define your own unit-quantifier names if you want
  288. # to use other decimal units (e.g.: 1500 becomes "1.5
  289. # kilometers", 0.150 becomes "150 milliliters", etc). You may
  290. # define a wide range of unit quantifiers, even fractional ones
  291. # (centi, deci, mili, etc).
  292. #
  293. # ==== Options
  294. #
  295. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  296. # (defaults to current locale).
  297. # * <tt>:precision</tt> - Sets the precision of the number
  298. # (defaults to 3).
  299. # * <tt>:round_mode</tt> - Determine how rounding is performed
  300. # (defaults to :default. See BigDecimal::mode)
  301. # * <tt>:significant</tt> - If +true+, precision will be the number
  302. # of significant_digits. If +false+, the number of fractional
  303. # digits (defaults to +true+)
  304. # * <tt>:separator</tt> - Sets the separator between the
  305. # fractional and integer digits (defaults to ".").
  306. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  307. # to "").
  308. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  309. # insignificant zeros after the decimal separator (defaults to
  310. # +true+)
  311. # * <tt>:units</tt> - A Hash of unit quantifier names. Or a
  312. # string containing an i18n scope where to find this hash. It
  313. # might have the following keys:
  314. # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
  315. # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
  316. # <tt>:billion</tt>, <tt>:trillion</tt>,
  317. # <tt>:quadrillion</tt>
  318. # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
  319. # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
  320. # <tt>:pico</tt>, <tt>:femto</tt>
  321. # * <tt>:format</tt> - Sets the format of the output string
  322. # (defaults to "%n %u"). The field types are:
  323. # * %u - The quantifier (ex.: 'thousand')
  324. # * %n - The number
  325. #
  326. # ==== Examples
  327. #
  328. # number_to_human(123) # => "123"
  329. # number_to_human(1234) # => "1.23 Thousand"
  330. # number_to_human(12345) # => "12.3 Thousand"
  331. # number_to_human(1234567) # => "1.23 Million"
  332. # number_to_human(1234567890) # => "1.23 Billion"
  333. # number_to_human(1234567890123) # => "1.23 Trillion"
  334. # number_to_human(1234567890123456) # => "1.23 Quadrillion"
  335. # number_to_human(1234567890123456789) # => "1230 Quadrillion"
  336. # number_to_human(489939, precision: 2) # => "490 Thousand"
  337. # number_to_human(489939, precision: 4) # => "489.9 Thousand"
  338. # number_to_human(489939, precision: 2
  339. # , round_mode: :down) # => "480 Thousand"
  340. # number_to_human(1234567, precision: 4,
  341. # significant: false) # => "1.2346 Million"
  342. # number_to_human(1234567, precision: 1,
  343. # separator: ',',
  344. # significant: false) # => "1,2 Million"
  345. #
  346. # number_to_human(500000000, precision: 5) # => "500 Million"
  347. # number_to_human(12345012345, significant: false) # => "12.345 Billion"
  348. #
  349. # Non-significant zeros after the decimal separator are stripped
  350. # out by default (set <tt>:strip_insignificant_zeros</tt> to
  351. # +false+ to change that):
  352. #
  353. # number_to_human(12.00001) # => "12"
  354. # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
  355. #
  356. # ==== Custom Unit Quantifiers
  357. #
  358. # You can also use your own custom unit quantifiers:
  359. # number_to_human(500000, units: { unit: 'ml', thousand: 'lt' }) # => "500 lt"
  360. #
  361. # If in your I18n locale you have:
  362. #
  363. # distance:
  364. # centi:
  365. # one: "centimeter"
  366. # other: "centimeters"
  367. # unit:
  368. # one: "meter"
  369. # other: "meters"
  370. # thousand:
  371. # one: "kilometer"
  372. # other: "kilometers"
  373. # billion: "gazillion-distance"
  374. #
  375. # Then you could do:
  376. #
  377. # number_to_human(543934, units: :distance) # => "544 kilometers"
  378. # number_to_human(54393498, units: :distance) # => "54400 kilometers"
  379. # number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance"
  380. # number_to_human(343, units: :distance, precision: 1) # => "300 meters"
  381. # number_to_human(1, units: :distance) # => "1 meter"
  382. # number_to_human(0.34, units: :distance) # => "34 centimeters"
  383. 1 def number_to_human(number, options = {})
  384. NumberToHumanConverter.convert(number, options)
  385. end
  386. end
  387. end

lib/active_support/number_helper/number_converter.rb

0.0% lines covered

125 relevant lines. 0 lines covered and 125 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/big_decimal/conversions"
  3. require "active_support/core_ext/object/blank"
  4. require "active_support/core_ext/hash/keys"
  5. require "active_support/i18n"
  6. require "active_support/core_ext/class/attribute"
  7. module ActiveSupport
  8. module NumberHelper
  9. class NumberConverter # :nodoc:
  10. # Default and i18n option namespace per class
  11. class_attribute :namespace
  12. # Does the object need a number that is a valid float?
  13. class_attribute :validate_float
  14. attr_reader :number, :opts
  15. DEFAULTS = {
  16. # Used in number_to_delimited
  17. # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
  18. format: {
  19. # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
  20. separator: ".",
  21. # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
  22. delimiter: ",",
  23. # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
  24. precision: 3,
  25. # If set to true, precision will mean the number of significant digits instead
  26. # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
  27. significant: false,
  28. # If set, the zeros after the decimal separator will always be stripped (e.g.: 1.200 will be 1.2)
  29. strip_insignificant_zeros: false
  30. },
  31. # Used in number_to_currency
  32. currency: {
  33. format: {
  34. format: "%u%n",
  35. negative_format: "-%u%n",
  36. unit: "$",
  37. # These five are to override number.format and are optional
  38. separator: ".",
  39. delimiter: ",",
  40. precision: 2,
  41. significant: false,
  42. strip_insignificant_zeros: false
  43. }
  44. },
  45. # Used in number_to_percentage
  46. percentage: {
  47. format: {
  48. delimiter: "",
  49. format: "%n%"
  50. }
  51. },
  52. # Used in number_to_rounded
  53. precision: {
  54. format: {
  55. delimiter: ""
  56. }
  57. },
  58. # Used in number_to_human_size and number_to_human
  59. human: {
  60. format: {
  61. # These five are to override number.format and are optional
  62. delimiter: "",
  63. precision: 3,
  64. significant: true,
  65. strip_insignificant_zeros: true
  66. },
  67. # Used in number_to_human_size
  68. storage_units: {
  69. # Storage units output formatting.
  70. # %u is the storage unit, %n is the number (default: 2 MB)
  71. format: "%n %u",
  72. units: {
  73. byte: "Bytes",
  74. kb: "KB",
  75. mb: "MB",
  76. gb: "GB",
  77. tb: "TB"
  78. }
  79. },
  80. # Used in number_to_human
  81. decimal_units: {
  82. format: "%n %u",
  83. # Decimal units output formatting
  84. # By default we will only quantify some of the exponents
  85. # but the commented ones might be defined or overridden
  86. # by the user.
  87. units: {
  88. # femto: Quadrillionth
  89. # pico: Trillionth
  90. # nano: Billionth
  91. # micro: Millionth
  92. # mili: Thousandth
  93. # centi: Hundredth
  94. # deci: Tenth
  95. unit: "",
  96. # ten:
  97. # one: Ten
  98. # other: Tens
  99. # hundred: Hundred
  100. thousand: "Thousand",
  101. million: "Million",
  102. billion: "Billion",
  103. trillion: "Trillion",
  104. quadrillion: "Quadrillion"
  105. }
  106. }
  107. }
  108. }
  109. def self.convert(number, options)
  110. new(number, options).execute
  111. end
  112. def initialize(number, options)
  113. @number = number
  114. @opts = options.symbolize_keys
  115. end
  116. def execute
  117. if !number
  118. nil
  119. elsif validate_float? && !valid_float?
  120. number
  121. else
  122. convert
  123. end
  124. end
  125. private
  126. def options
  127. @options ||= format_options.merge(opts)
  128. end
  129. def format_options
  130. default_format_options.merge!(i18n_format_options)
  131. end
  132. def default_format_options
  133. options = DEFAULTS[:format].dup
  134. options.merge!(DEFAULTS[namespace][:format]) if namespace
  135. options
  136. end
  137. def i18n_format_options
  138. locale = opts[:locale]
  139. options = I18n.translate(:'number.format', locale: locale, default: {}).dup
  140. if namespace
  141. options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
  142. end
  143. options
  144. end
  145. def translate_number_value_with_default(key, **i18n_options)
  146. I18n.translate(key, **{ default: default_value(key), scope: :number }.merge!(i18n_options))
  147. end
  148. def translate_in_locale(key, **i18n_options)
  149. translate_number_value_with_default(key, **{ locale: options[:locale] }.merge(i18n_options))
  150. end
  151. def default_value(key)
  152. key.split(".").reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
  153. end
  154. def valid_float?
  155. Float(number)
  156. rescue ArgumentError, TypeError
  157. false
  158. end
  159. end
  160. end
  161. end

lib/active_support/number_helper/number_to_currency_converter.rb

0.0% lines covered

34 relevant lines. 0 lines covered and 34 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToCurrencyConverter < NumberConverter # :nodoc:
  6. self.namespace = :currency
  7. def convert
  8. number = self.number.to_s.strip
  9. number_f = number.to_f
  10. format = options[:format]
  11. if number_f.negative?
  12. number = number_f.abs
  13. unless options[:precision] == 0 && number < 0.5
  14. format = options[:negative_format]
  15. end
  16. end
  17. rounded_number = NumberToRoundedConverter.convert(number, options)
  18. format.gsub("%n", rounded_number).gsub("%u", options[:unit])
  19. end
  20. private
  21. def options
  22. @options ||= begin
  23. defaults = default_format_options.merge(i18n_opts)
  24. # Override negative format if format options are given
  25. defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
  26. defaults.merge!(opts)
  27. end
  28. end
  29. def i18n_opts
  30. # Set International negative format if it does not exist
  31. i18n = i18n_format_options
  32. i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
  33. i18n
  34. end
  35. end
  36. end
  37. end

lib/active_support/number_helper/number_to_delimited_converter.rb

0.0% lines covered

23 relevant lines. 0 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToDelimitedConverter < NumberConverter #:nodoc:
  6. self.validate_float = true
  7. DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
  8. def convert
  9. parts.join(options[:separator])
  10. end
  11. private
  12. def parts
  13. left, right = number.to_s.split(".")
  14. left.gsub!(delimiter_pattern) do |digit_to_delimit|
  15. "#{digit_to_delimit}#{options[:delimiter]}"
  16. end
  17. [left, right].compact
  18. end
  19. def delimiter_pattern
  20. options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX)
  21. end
  22. end
  23. end
  24. end

lib/active_support/number_helper/number_to_human_converter.rb

0.0% lines covered

56 relevant lines. 0 lines covered and 56 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToHumanConverter < NumberConverter # :nodoc:
  6. DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
  7. -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
  8. INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
  9. self.namespace = :human
  10. self.validate_float = true
  11. def convert # :nodoc:
  12. @number = RoundingHelper.new(options).round(number)
  13. @number = Float(number)
  14. # For backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files.
  15. unless options.key?(:strip_insignificant_zeros)
  16. options[:strip_insignificant_zeros] = true
  17. end
  18. units = opts[:units]
  19. exponent = calculate_exponent(units)
  20. @number = number / (10**exponent)
  21. rounded_number = NumberToRoundedConverter.convert(number, options)
  22. unit = determine_unit(units, exponent)
  23. format.gsub("%n", rounded_number).gsub("%u", unit).strip
  24. end
  25. private
  26. def format
  27. options[:format] || translate_in_locale("human.decimal_units.format")
  28. end
  29. def determine_unit(units, exponent)
  30. exp = DECIMAL_UNITS[exponent]
  31. case units
  32. when Hash
  33. units[exp] || ""
  34. when String, Symbol
  35. I18n.translate("#{units}.#{exp}", locale: options[:locale], count: number.to_i)
  36. else
  37. translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
  38. end
  39. end
  40. def calculate_exponent(units)
  41. exponent = number != 0 ? Math.log10(number.abs).floor : 0
  42. unit_exponents(units).find { |e| exponent >= e } || 0
  43. end
  44. def unit_exponents(units)
  45. case units
  46. when Hash
  47. units
  48. when String, Symbol
  49. I18n.translate(units.to_s, locale: options[:locale], raise: true)
  50. when nil
  51. translate_in_locale("human.decimal_units.units", raise: true)
  52. else
  53. raise ArgumentError, ":units must be a Hash or String translation scope."
  54. end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
  55. end
  56. end
  57. end
  58. end

lib/active_support/number_helper/number_to_human_size_converter.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToHumanSizeConverter < NumberConverter #:nodoc:
  6. STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb]
  7. self.namespace = :human
  8. self.validate_float = true
  9. def convert
  10. @number = Float(number)
  11. # For backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files.
  12. unless options.key?(:strip_insignificant_zeros)
  13. options[:strip_insignificant_zeros] = true
  14. end
  15. if smaller_than_base?
  16. number_to_format = number.to_i.to_s
  17. else
  18. human_size = number / (base**exponent)
  19. number_to_format = NumberToRoundedConverter.convert(human_size, options)
  20. end
  21. conversion_format.gsub("%n", number_to_format).gsub("%u", unit)
  22. end
  23. private
  24. def conversion_format
  25. translate_number_value_with_default("human.storage_units.format", locale: options[:locale], raise: true)
  26. end
  27. def unit
  28. translate_number_value_with_default(storage_unit_key, locale: options[:locale], count: number.to_i, raise: true)
  29. end
  30. def storage_unit_key
  31. key_end = smaller_than_base? ? "byte" : STORAGE_UNITS[exponent]
  32. "human.storage_units.units.#{key_end}"
  33. end
  34. def exponent
  35. max = STORAGE_UNITS.size - 1
  36. exp = (Math.log(number) / Math.log(base)).to_i
  37. exp = max if exp > max # avoid overflow for the highest unit
  38. exp
  39. end
  40. def smaller_than_base?
  41. number.to_i < base
  42. end
  43. def base
  44. 1024
  45. end
  46. end
  47. end
  48. end

lib/active_support/number_helper/number_to_percentage_converter.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToPercentageConverter < NumberConverter # :nodoc:
  6. self.namespace = :percentage
  7. def convert
  8. rounded_number = NumberToRoundedConverter.convert(number, options)
  9. options[:format].gsub("%n", rounded_number)
  10. end
  11. end
  12. end
  13. end

lib/active_support/number_helper/number_to_phone_converter.rb

0.0% lines covered

48 relevant lines. 0 lines covered and 48 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToPhoneConverter < NumberConverter #:nodoc:
  6. def convert
  7. str = country_code(opts[:country_code]).dup
  8. str << convert_to_phone_number(number.to_s.strip)
  9. str << phone_ext(opts[:extension])
  10. end
  11. private
  12. def convert_to_phone_number(number)
  13. if opts[:area_code]
  14. convert_with_area_code(number)
  15. else
  16. convert_without_area_code(number)
  17. end
  18. end
  19. def convert_with_area_code(number)
  20. default_pattern = /(\d{1,3})(\d{3})(\d{4}$)/
  21. number.gsub!(regexp_pattern(default_pattern),
  22. "(\\1) \\2#{delimiter}\\3")
  23. number
  24. end
  25. def convert_without_area_code(number)
  26. default_pattern = /(\d{0,3})(\d{3})(\d{4})$/
  27. number.gsub!(regexp_pattern(default_pattern),
  28. "\\1#{delimiter}\\2#{delimiter}\\3")
  29. number.slice!(0, 1) if start_with_delimiter?(number)
  30. number
  31. end
  32. def start_with_delimiter?(number)
  33. delimiter.present? && number.start_with?(delimiter)
  34. end
  35. def delimiter
  36. opts[:delimiter] || "-"
  37. end
  38. def country_code(code)
  39. code.blank? ? "" : "+#{code}#{delimiter}"
  40. end
  41. def phone_ext(ext)
  42. ext.blank? ? "" : " x #{ext}"
  43. end
  44. def regexp_pattern(default_pattern)
  45. opts.fetch :pattern, default_pattern
  46. end
  47. end
  48. end
  49. end

lib/active_support/number_helper/number_to_rounded_converter.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/number_helper/number_converter"
  3. module ActiveSupport
  4. module NumberHelper
  5. class NumberToRoundedConverter < NumberConverter # :nodoc:
  6. self.namespace = :precision
  7. self.validate_float = true
  8. def convert
  9. helper = RoundingHelper.new(options)
  10. rounded_number = helper.round(number)
  11. if precision = options[:precision]
  12. if options[:significant] && precision > 0
  13. digits = helper.digit_count(rounded_number)
  14. precision -= digits
  15. precision = 0 if precision < 0 # don't let it be negative
  16. end
  17. formatted_string =
  18. if rounded_number.nan? || rounded_number.infinite? || rounded_number == rounded_number.to_i
  19. "%00.#{precision}f" % rounded_number
  20. else
  21. s = rounded_number.to_s("F")
  22. s << "0" * precision
  23. a, b = s.split(".", 2)
  24. a << "."
  25. a << b[0, precision]
  26. end
  27. else
  28. formatted_string = rounded_number
  29. end
  30. delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
  31. format_number(delimited_number)
  32. end
  33. private
  34. def strip_insignificant_zeros
  35. options[:strip_insignificant_zeros]
  36. end
  37. def format_number(number)
  38. if strip_insignificant_zeros
  39. escaped_separator = Regexp.escape(options[:separator])
  40. number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, "")
  41. else
  42. number
  43. end
  44. end
  45. end
  46. end
  47. end

lib/active_support/number_helper/rounding_helper.rb

0.0% lines covered

41 relevant lines. 0 lines covered and 41 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. module NumberHelper
  4. class RoundingHelper # :nodoc:
  5. attr_reader :options
  6. def initialize(options)
  7. @options = options
  8. end
  9. def round(number)
  10. precision = absolute_precision(number)
  11. return number unless precision
  12. rounded_number = convert_to_decimal(number).round(precision, options.fetch(:round_mode, :default))
  13. rounded_number.zero? ? rounded_number.abs : rounded_number # prevent showing negative zeros
  14. end
  15. def digit_count(number)
  16. return 1 if number.zero?
  17. (Math.log10(number.abs) + 1).floor
  18. end
  19. private
  20. def convert_to_decimal(number)
  21. case number
  22. when Float, String
  23. BigDecimal(number.to_s)
  24. when Rational
  25. BigDecimal(number, digit_count(number.to_i) + options[:precision])
  26. else
  27. number.to_d
  28. end
  29. end
  30. def absolute_precision(number)
  31. if significant && options[:precision] > 0
  32. options[:precision] - digit_count(convert_to_decimal(number))
  33. else
  34. options[:precision]
  35. end
  36. end
  37. def significant
  38. options[:significant]
  39. end
  40. end
  41. end
  42. end

lib/active_support/option_merger.rb

42.31% lines covered

26 relevant lines. 11 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/hash/deep_merge"
  3. 2 require "active_support/core_ext/symbol/starts_ends_with"
  4. 2 module ActiveSupport
  5. 2 class OptionMerger #:nodoc:
  6. 2 instance_methods.each do |method|
  7. 152 undef_method(method) unless method.start_with?("__", "instance_eval", "class", "object_id")
  8. end
  9. 2 def initialize(context, options)
  10. @context, @options = context, options
  11. end
  12. 2 private
  13. 2 def method_missing(method, *arguments, &block)
  14. options = nil
  15. if arguments.first.is_a?(Proc)
  16. proc = arguments.pop
  17. arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
  18. elsif arguments.last.respond_to?(:to_hash)
  19. options = @options.deep_merge(arguments.pop)
  20. else
  21. options = @options
  22. end
  23. invoke_method(method, arguments, options, &block)
  24. end
  25. 2 if RUBY_VERSION >= "2.7"
  26. def invoke_method(method, arguments, options, &block)
  27. if options
  28. @context.__send__(method, *arguments, **options, &block)
  29. else
  30. @context.__send__(method, *arguments, &block)
  31. end
  32. end
  33. else
  34. 2 def invoke_method(method, arguments, options, &block)
  35. arguments << options.dup if options
  36. @context.__send__(method, *arguments, &block)
  37. end
  38. end
  39. end
  40. end

lib/active_support/ordered_hash.rb

58.82% lines covered

17 relevant lines. 10 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "yaml"
  3. 1 YAML.add_builtin_type("omap") do |type, val|
  4. ActiveSupport::OrderedHash[val.map { |v| v.to_a.first }]
  5. end
  6. 1 module ActiveSupport
  7. # DEPRECATED: <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves
  8. # insertion order.
  9. #
  10. # oh = ActiveSupport::OrderedHash.new
  11. # oh[:a] = 1
  12. # oh[:b] = 2
  13. # oh.keys # => [:a, :b], this order is guaranteed
  14. #
  15. # Also, maps the +omap+ feature for YAML files
  16. # (See https://yaml.org/type/omap.html) to support ordered items
  17. # when loading from yaml.
  18. #
  19. # <tt>ActiveSupport::OrderedHash</tt> is namespaced to prevent conflicts
  20. # with other implementations.
  21. 1 class OrderedHash < ::Hash
  22. 1 def to_yaml_type
  23. "!tag:yaml.org,2002:omap"
  24. end
  25. 1 def encode_with(coder)
  26. coder.represent_seq "!omap", map { |k, v| { k => v } }
  27. end
  28. 1 def select(*args, &block)
  29. dup.tap { |hash| hash.select!(*args, &block) }
  30. end
  31. 1 def reject(*args, &block)
  32. dup.tap { |hash| hash.reject!(*args, &block) }
  33. end
  34. 1 def nested_under_indifferent_access
  35. self
  36. end
  37. # Returns true to make sure that this hash is extractable via <tt>Array#extract_options!</tt>
  38. 1 def extractable_options?
  39. true
  40. end
  41. end
  42. end

lib/active_support/ordered_options.rb

43.33% lines covered

30 relevant lines. 13 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 require "active_support/core_ext/object/blank"
  3. 2 module ActiveSupport
  4. # +OrderedOptions+ inherits from +Hash+ and provides dynamic accessor methods.
  5. #
  6. # With a +Hash+, key-value pairs are typically managed like this:
  7. #
  8. # h = {}
  9. # h[:boy] = 'John'
  10. # h[:girl] = 'Mary'
  11. # h[:boy] # => 'John'
  12. # h[:girl] # => 'Mary'
  13. # h[:dog] # => nil
  14. #
  15. # Using +OrderedOptions+, the above code can be written as:
  16. #
  17. # h = ActiveSupport::OrderedOptions.new
  18. # h.boy = 'John'
  19. # h.girl = 'Mary'
  20. # h.boy # => 'John'
  21. # h.girl # => 'Mary'
  22. # h.dog # => nil
  23. #
  24. # To raise an exception when the value is blank, append a
  25. # bang to the key name, like:
  26. #
  27. # h.dog! # => raises KeyError: :dog is blank
  28. #
  29. 2 class OrderedOptions < Hash
  30. 2 alias_method :_get, :[] # preserve the original #[] method
  31. 2 protected :_get # make it protected
  32. 2 def []=(key, value)
  33. super(key.to_sym, value)
  34. end
  35. 2 def [](key)
  36. super(key.to_sym)
  37. end
  38. 2 def method_missing(name, *args)
  39. name_string = +name.to_s
  40. if name_string.chomp!("=")
  41. self[name_string] = args.first
  42. else
  43. bangs = name_string.chomp!("!")
  44. if bangs
  45. self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
  46. else
  47. self[name_string]
  48. end
  49. end
  50. end
  51. 2 def respond_to_missing?(name, include_private)
  52. true
  53. end
  54. 2 def extractable_options?
  55. true
  56. end
  57. end
  58. # +InheritableOptions+ provides a constructor to build an +OrderedOptions+
  59. # hash inherited from another hash.
  60. #
  61. # Use this if you already have some hash and you want to create a new one based on it.
  62. #
  63. # h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' })
  64. # h.girl # => 'Mary'
  65. # h.boy # => 'John'
  66. 2 class InheritableOptions < OrderedOptions
  67. 2 def initialize(parent = nil)
  68. if parent.kind_of?(OrderedOptions)
  69. # use the faster _get when dealing with OrderedOptions
  70. super() { |h, k| parent._get(k) }
  71. elsif parent
  72. super() { |h, k| parent[k] }
  73. else
  74. super()
  75. end
  76. end
  77. 2 def inheritable_copy
  78. self.class.new(self)
  79. end
  80. end
  81. end

lib/active_support/parameter_filter.rb

25.0% lines covered

60 relevant lines. 15 lines covered and 45 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/duplicable"
  3. 1 module ActiveSupport
  4. # +ParameterFilter+ allows you to specify keys for sensitive data from
  5. # hash-like object and replace corresponding value. Filtering only certain
  6. # sub-keys from a hash is possible by using the dot notation:
  7. # 'credit_card.number'. If a proc is given, each key and value of a hash and
  8. # all sub-hashes are passed to it, where the value or the key can be replaced
  9. # using String#replace or similar methods.
  10. #
  11. # ActiveSupport::ParameterFilter.new([:password])
  12. # => replaces the value to all keys matching /password/i with "[FILTERED]"
  13. #
  14. # ActiveSupport::ParameterFilter.new([:foo, "bar"])
  15. # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
  16. #
  17. # ActiveSupport::ParameterFilter.new(["credit_card.code"])
  18. # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
  19. # change { file: { code: "xxxx"} }
  20. #
  21. # ActiveSupport::ParameterFilter.new([-> (k, v) do
  22. # v.reverse! if /secret/i.match?(k)
  23. # end])
  24. # => reverses the value to all keys matching /secret/i
  25. 1 class ParameterFilter
  26. 1 FILTERED = "[FILTERED]" # :nodoc:
  27. # Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+.
  28. # Other types of filters are treated as +String+ using +to_s+.
  29. # For +Proc+ filters, key, value, and optional original hash is passed to block arguments.
  30. #
  31. # ==== Options
  32. #
  33. # * <tt>:mask</tt> - A replaced object when filtered. Defaults to +"[FILTERED]"+
  34. 1 def initialize(filters = [], mask: FILTERED)
  35. @filters = filters
  36. @mask = mask
  37. end
  38. # Mask value of +params+ if key matches one of filters.
  39. 1 def filter(params)
  40. compiled_filter.call(params)
  41. end
  42. # Returns filtered value for given key. For +Proc+ filters, third block argument is not populated.
  43. 1 def filter_param(key, value)
  44. @filters.empty? ? value : compiled_filter.value_for_key(key, value)
  45. end
  46. 1 private
  47. 1 def compiled_filter
  48. @compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask)
  49. end
  50. 1 class CompiledFilter # :nodoc:
  51. 1 def self.compile(filters, mask:)
  52. return lambda { |params| params.dup } if filters.empty?
  53. strings, regexps, blocks, deep_regexps, deep_strings = [], [], [], nil, nil
  54. filters.each do |item|
  55. case item
  56. when Proc
  57. blocks << item
  58. when Regexp
  59. if item.to_s.include?("\\.")
  60. (deep_regexps ||= []) << item
  61. else
  62. regexps << item
  63. end
  64. else
  65. s = Regexp.escape(item.to_s)
  66. if s.include?("\\.")
  67. (deep_strings ||= []) << s
  68. else
  69. strings << s
  70. end
  71. end
  72. end
  73. regexps << Regexp.new(strings.join("|"), true) unless strings.empty?
  74. (deep_regexps ||= []) << Regexp.new(deep_strings.join("|"), true) if deep_strings&.any?
  75. new regexps, deep_regexps, blocks, mask: mask
  76. end
  77. 1 attr_reader :regexps, :deep_regexps, :blocks
  78. 1 def initialize(regexps, deep_regexps, blocks, mask:)
  79. @regexps = regexps
  80. @deep_regexps = deep_regexps&.any? ? deep_regexps : nil
  81. @blocks = blocks
  82. @mask = mask
  83. end
  84. 1 def call(params, parents = [], original_params = params)
  85. filtered_params = params.class.new
  86. params.each do |key, value|
  87. filtered_params[key] = value_for_key(key, value, parents, original_params)
  88. end
  89. filtered_params
  90. end
  91. 1 def value_for_key(key, value, parents = [], original_params = nil)
  92. parents.push(key) if deep_regexps
  93. if regexps.any? { |r| r.match?(key.to_s) }
  94. value = @mask
  95. elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) }
  96. value = @mask
  97. elsif value.is_a?(Hash)
  98. value = call(value, parents, original_params)
  99. elsif value.is_a?(Array)
  100. # If we don't pop the current parent it will be duplicated as we
  101. # process each array value.
  102. parents.pop if deep_regexps
  103. value = value.map { |v| value_for_key(key, v, parents, original_params) }
  104. # Restore the parent stack after processing the array.
  105. parents.push(key) if deep_regexps
  106. elsif blocks.any?
  107. key = key.dup if key.duplicable?
  108. value = value.dup if value.duplicable?
  109. blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) }
  110. end
  111. parents.pop if deep_regexps
  112. value
  113. end
  114. end
  115. end
  116. end

lib/active_support/per_thread_registry.rb

72.73% lines covered

11 relevant lines. 8 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "active_support/core_ext/module/delegation"
  3. 24 module ActiveSupport
  4. # NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends.
  5. # Please use that approach instead.
  6. #
  7. # This module is used to encapsulate access to thread local variables.
  8. #
  9. # Instead of polluting the thread locals namespace:
  10. #
  11. # Thread.current[:connection_handler]
  12. #
  13. # you define a class that extends this module:
  14. #
  15. # module ActiveRecord
  16. # class RuntimeRegistry
  17. # extend ActiveSupport::PerThreadRegistry
  18. #
  19. # attr_accessor :connection_handler
  20. # end
  21. # end
  22. #
  23. # and invoke the declared instance accessors as class methods. So
  24. #
  25. # ActiveRecord::RuntimeRegistry.connection_handler = connection_handler
  26. #
  27. # sets a connection handler local to the current thread, and
  28. #
  29. # ActiveRecord::RuntimeRegistry.connection_handler
  30. #
  31. # returns a connection handler local to the current thread.
  32. #
  33. # This feature is accomplished by instantiating the class and storing the
  34. # instance as a thread local keyed by the class name. In the example above
  35. # a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>.
  36. # The class methods proxy to said thread local instance.
  37. #
  38. # If the class has an initializer, it must accept no arguments.
  39. 24 module PerThreadRegistry
  40. 24 def self.extended(object)
  41. 29 object.instance_variable_set :@per_thread_registry_key, object.name.freeze
  42. end
  43. 24 def instance
  44. Thread.current[@per_thread_registry_key] ||= new
  45. end
  46. 24 private
  47. 24 def method_missing(name, *args, &block)
  48. # Caches the method definition as a singleton method of the receiver.
  49. #
  50. # By letting #delegate handle it, we avoid an enclosure that'll capture args.
  51. singleton_class.delegate name, to: :instance
  52. send(name, *args, &block)
  53. end
  54. end
  55. end

lib/active_support/proxy_object.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActiveSupport
  3. # A class with no predefined methods that behaves similarly to Builder's
  4. # BlankSlate. Used for proxy classes.
  5. class ProxyObject < ::BasicObject
  6. undef_method :==
  7. undef_method :equal?
  8. # Let ActiveSupport::ProxyObject at least raise exceptions.
  9. def raise(*args)
  10. ::Object.send(:raise, *args)
  11. end
  12. end
  13. end

lib/active_support/rails.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. # This is a private interface.
  3. #
  4. # Rails components cherry pick from Active Support as needed, but there are a
  5. # few features that are used for sure in some way or another and it is not worth
  6. # putting individual requires absolutely everywhere. Think blank? for example.
  7. #
  8. # This file is loaded by every Rails component except Active Support itself,
  9. # but it does not belong to the Rails public interface. It is internal to
  10. # Rails and can change anytime.
  11. # Defines Object#blank? and Object#present?.
  12. require "active_support/core_ext/object/blank"
  13. # Support for ClassMethods and the included macro.
  14. require "active_support/concern"
  15. # Defines Class#class_attribute.
  16. require "active_support/core_ext/class/attribute"
  17. # Defines Module#delegate.
  18. require "active_support/core_ext/module/delegation"
  19. # Defines ActiveSupport::Deprecation.
  20. require "active_support/deprecation"

lib/active_support/railtie.rb

0.0% lines covered

73 relevant lines. 0 lines covered and 73 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support"
  3. require "active_support/i18n_railtie"
  4. module ActiveSupport
  5. class Railtie < Rails::Railtie # :nodoc:
  6. config.active_support = ActiveSupport::OrderedOptions.new
  7. config.eager_load_namespaces << ActiveSupport
  8. initializer "active_support.set_authenticated_message_encryption" do |app|
  9. config.after_initialize do
  10. unless app.config.active_support.use_authenticated_message_encryption.nil?
  11. ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
  12. app.config.active_support.use_authenticated_message_encryption
  13. end
  14. end
  15. end
  16. initializer "active_support.reset_all_current_attributes_instances" do |app|
  17. app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
  18. app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all }
  19. app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all }
  20. ActiveSupport.on_load(:active_support_test_case) do
  21. require "active_support/current_attributes/test_helper"
  22. include ActiveSupport::CurrentAttributes::TestHelper
  23. end
  24. end
  25. initializer "active_support.deprecation_behavior" do |app|
  26. if deprecation = app.config.active_support.deprecation
  27. ActiveSupport::Deprecation.behavior = deprecation
  28. end
  29. if disallowed_deprecation = app.config.active_support.disallowed_deprecation
  30. ActiveSupport::Deprecation.disallowed_behavior = disallowed_deprecation
  31. end
  32. if disallowed_warnings = app.config.active_support.disallowed_deprecation_warnings
  33. ActiveSupport::Deprecation.disallowed_warnings = disallowed_warnings
  34. end
  35. end
  36. # Sets the default value for Time.zone
  37. # If assigned value cannot be matched to a TimeZone, an exception will be raised.
  38. initializer "active_support.initialize_time_zone" do |app|
  39. begin
  40. TZInfo::DataSource.get
  41. rescue TZInfo::DataSourceNotFound => e
  42. raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
  43. end
  44. require "active_support/core_ext/time/zones"
  45. Time.zone_default = Time.find_zone!(app.config.time_zone)
  46. end
  47. # Sets the default week start
  48. # If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
  49. initializer "active_support.initialize_beginning_of_week" do |app|
  50. require "active_support/core_ext/date/calculations"
  51. beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week)
  52. Date.beginning_of_week_default = beginning_of_week_default
  53. end
  54. initializer "active_support.require_master_key" do |app|
  55. if app.config.respond_to?(:require_master_key) && app.config.require_master_key
  56. begin
  57. app.credentials.key
  58. rescue ActiveSupport::EncryptedFile::MissingKeyError => error
  59. $stderr.puts error.message
  60. exit 1
  61. end
  62. end
  63. end
  64. initializer "active_support.set_configs" do |app|
  65. app.config.active_support.each do |k, v|
  66. k = "#{k}="
  67. ActiveSupport.send(k, v) if ActiveSupport.respond_to? k
  68. end
  69. end
  70. initializer "active_support.set_hash_digest_class" do |app|
  71. config.after_initialize do
  72. if app.config.active_support.use_sha1_digests
  73. ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
  74. end
  75. end
  76. end
  77. end
  78. end

lib/active_support/reloader.rb

0.0% lines covered

81 relevant lines. 0 lines covered and 81 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/execution_wrapper"
  3. require "active_support/executor"
  4. module ActiveSupport
  5. #--
  6. # This class defines several callbacks:
  7. #
  8. # to_prepare -- Run once at application startup, and also from
  9. # +to_run+.
  10. #
  11. # to_run -- Run before a work run that is reloading. If
  12. # +reload_classes_only_on_change+ is true (the default), the class
  13. # unload will have already occurred.
  14. #
  15. # to_complete -- Run after a work run that has reloaded. If
  16. # +reload_classes_only_on_change+ is false, the class unload will
  17. # have occurred after the work run, but before this callback.
  18. #
  19. # before_class_unload -- Run immediately before the classes are
  20. # unloaded.
  21. #
  22. # after_class_unload -- Run immediately after the classes are
  23. # unloaded.
  24. #
  25. class Reloader < ExecutionWrapper
  26. define_callbacks :prepare
  27. define_callbacks :class_unload
  28. # Registers a callback that will run once at application startup and every time the code is reloaded.
  29. def self.to_prepare(*args, &block)
  30. set_callback(:prepare, *args, &block)
  31. end
  32. # Registers a callback that will run immediately before the classes are unloaded.
  33. def self.before_class_unload(*args, &block)
  34. set_callback(:class_unload, *args, &block)
  35. end
  36. # Registers a callback that will run immediately after the classes are unloaded.
  37. def self.after_class_unload(*args, &block)
  38. set_callback(:class_unload, :after, *args, &block)
  39. end
  40. to_run(:after) { self.class.prepare! }
  41. # Initiate a manual reload
  42. def self.reload!
  43. executor.wrap do
  44. new.tap do |instance|
  45. instance.run!
  46. ensure
  47. instance.complete!
  48. end
  49. end
  50. prepare!
  51. end
  52. def self.run! # :nodoc:
  53. if check!
  54. super
  55. else
  56. Null
  57. end
  58. end
  59. # Run the supplied block as a work unit, reloading code as needed
  60. def self.wrap
  61. executor.wrap do
  62. super
  63. end
  64. end
  65. class_attribute :executor, default: Executor
  66. class_attribute :check, default: lambda { false }
  67. def self.check! # :nodoc:
  68. @should_reload ||= check.call
  69. end
  70. def self.reloaded! # :nodoc:
  71. @should_reload = false
  72. end
  73. def self.prepare! # :nodoc:
  74. new.run_callbacks(:prepare)
  75. end
  76. def initialize
  77. super
  78. @locked = false
  79. end
  80. # Acquire the ActiveSupport::Dependencies::Interlock unload lock,
  81. # ensuring it will be released automatically
  82. def require_unload_lock!
  83. unless @locked
  84. ActiveSupport::Dependencies.interlock.start_unloading
  85. @locked = true
  86. end
  87. end
  88. # Release the unload lock if it has been previously obtained
  89. def release_unload_lock!
  90. if @locked
  91. @locked = false
  92. ActiveSupport::Dependencies.interlock.done_unloading
  93. end
  94. end
  95. def run! # :nodoc:
  96. super
  97. release_unload_lock!
  98. end
  99. def class_unload!(&block) # :nodoc:
  100. require_unload_lock!
  101. run_callbacks(:class_unload, &block)
  102. end
  103. def complete! # :nodoc:
  104. super
  105. self.class.reloaded!
  106. ensure
  107. release_unload_lock!
  108. end
  109. end
  110. end

lib/active_support/rescuable.rb

46.43% lines covered

56 relevant lines. 26 lines covered and 30 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/concern"
  3. 1 require "active_support/core_ext/class/attribute"
  4. 1 require "active_support/core_ext/string/inflections"
  5. 1 module ActiveSupport
  6. # Rescuable module adds support for easier exception handling.
  7. 1 module Rescuable
  8. 1 extend Concern
  9. 1 included do
  10. 1 class_attribute :rescue_handlers, default: []
  11. end
  12. 1 module ClassMethods
  13. # Rescue exceptions raised in controller actions.
  14. #
  15. # <tt>rescue_from</tt> receives a series of exception classes or class
  16. # names, and a trailing <tt>:with</tt> option with the name of a method
  17. # or a Proc object to be called to handle them. Alternatively a block can
  18. # be given.
  19. #
  20. # Handlers that take one argument will be called with the exception, so
  21. # that the exception can be inspected when dealing with it.
  22. #
  23. # Handlers are inherited. They are searched from right to left, from
  24. # bottom to top, and up the hierarchy. The handler of the first class for
  25. # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
  26. # any.
  27. #
  28. # class ApplicationController < ActionController::Base
  29. # rescue_from User::NotAuthorized, with: :deny_access # self defined exception
  30. # rescue_from ActiveRecord::RecordInvalid, with: :show_errors
  31. #
  32. # rescue_from 'MyAppError::Base' do |exception|
  33. # render xml: exception, status: 500
  34. # end
  35. #
  36. # private
  37. # def deny_access
  38. # ...
  39. # end
  40. #
  41. # def show_errors(exception)
  42. # exception.record.new_record? ? ...
  43. # end
  44. # end
  45. #
  46. # Exceptions raised inside exception handlers are not propagated up.
  47. 1 def rescue_from(*klasses, with: nil, &block)
  48. 6 unless with
  49. 3 if block_given?
  50. 3 with = block
  51. else
  52. raise ArgumentError, "Need a handler. Pass the with: keyword argument or provide a block."
  53. end
  54. end
  55. 6 klasses.each do |klass|
  56. 6 key = if klass.is_a?(Module) && klass.respond_to?(:===)
  57. 5 klass.name
  58. 1 elsif klass.is_a?(String)
  59. 1 klass
  60. else
  61. raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class"
  62. end
  63. # Put the new handler at the end because the list is read in reverse.
  64. 6 self.rescue_handlers += [[key, with]]
  65. end
  66. end
  67. # Matches an exception to a handler based on the exception class.
  68. #
  69. # If no handler matches the exception, check for a handler matching the
  70. # (optional) exception.cause. If no handler matches the exception or its
  71. # cause, this returns +nil+, so you can deal with unhandled exceptions.
  72. # Be sure to re-raise unhandled exceptions if this is what you expect.
  73. #
  74. # begin
  75. # …
  76. # rescue => exception
  77. # rescue_with_handler(exception) || raise
  78. # end
  79. #
  80. # Returns the exception if it was handled and +nil+ if it was not.
  81. 1 def rescue_with_handler(exception, object: self, visited_exceptions: [])
  82. visited_exceptions << exception
  83. if handler = handler_for_rescue(exception, object: object)
  84. handler.call exception
  85. exception
  86. elsif exception
  87. if visited_exceptions.include?(exception.cause)
  88. nil
  89. else
  90. rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
  91. end
  92. end
  93. end
  94. 1 def handler_for_rescue(exception, object: self) #:nodoc:
  95. case rescuer = find_rescue_handler(exception)
  96. when Symbol
  97. method = object.method(rescuer)
  98. if method.arity == 0
  99. -> e { method.call }
  100. else
  101. method
  102. end
  103. when Proc
  104. if rescuer.arity == 0
  105. -> e { object.instance_exec(&rescuer) }
  106. else
  107. -> e { object.instance_exec(e, &rescuer) }
  108. end
  109. end
  110. end
  111. 1 private
  112. 1 def find_rescue_handler(exception)
  113. if exception
  114. # Handlers are in order of declaration but the most recently declared
  115. # is the highest priority match, so we search for matching handlers
  116. # in reverse.
  117. _, handler = rescue_handlers.reverse_each.detect do |class_or_name, _|
  118. if klass = constantize_rescue_handler_class(class_or_name)
  119. klass === exception
  120. end
  121. end
  122. handler
  123. end
  124. end
  125. 1 def constantize_rescue_handler_class(class_or_name)
  126. case class_or_name
  127. when String, Symbol
  128. begin
  129. # Try a lexical lookup first since we support
  130. #
  131. # class Super
  132. # rescue_from 'Error', with: …
  133. # end
  134. #
  135. # class Sub
  136. # class Error < StandardError; end
  137. # end
  138. #
  139. # so an Error raised in Sub will hit the 'Error' handler.
  140. const_get class_or_name
  141. rescue NameError
  142. class_or_name.safe_constantize
  143. end
  144. else
  145. class_or_name
  146. end
  147. end
  148. end
  149. # Delegates to the class method, but uses the instance as the subject for
  150. # rescue_from handlers (method calls, instance_exec blocks).
  151. 1 def rescue_with_handler(exception)
  152. self.class.rescue_with_handler exception, object: self
  153. end
  154. # Internal handler lookup. Delegates to class method. Some libraries call
  155. # this directly, so keeping it around for compatibility.
  156. 1 def handler_for_rescue(exception) #:nodoc:
  157. self.class.handler_for_rescue exception, object: self
  158. end
  159. end
  160. end

lib/active_support/secure_compare_rotator.rb

73.33% lines covered

15 relevant lines. 11 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/security_utils"
  3. 1 require "active_support/messages/rotator"
  4. 1 module ActiveSupport
  5. # The ActiveSupport::SecureCompareRotator is a wrapper around +ActiveSupport::SecurityUtils.secure_compare+
  6. # and allows you to rotate a previously defined value to a new one.
  7. #
  8. # It can be used as follow:
  9. #
  10. # rotator = ActiveSupport::SecureCompareRotator.new('new_production_value')
  11. # rotator.rotate('previous_production_value')
  12. # rotator.secure_compare!('previous_production_value')
  13. #
  14. # One real use case example would be to rotate a basic auth credentials:
  15. #
  16. # class MyController < ApplicationController
  17. # def authenticate_request
  18. # rotator = ActiveSupport::SecureComparerotator.new('new_password')
  19. # rotator.rotate('old_password')
  20. #
  21. # authenticate_or_request_with_http_basic do |username, password|
  22. # rotator.secure_compare!(password)
  23. # rescue ActiveSupport::SecureCompareRotator::InvalidMatch
  24. # false
  25. # end
  26. # end
  27. # end
  28. 1 class SecureCompareRotator
  29. 1 include SecurityUtils
  30. 1 prepend Messages::Rotator
  31. 1 InvalidMatch = Class.new(StandardError)
  32. 1 def initialize(value, **_options)
  33. @value = value
  34. end
  35. 1 def secure_compare!(other_value, on_rotation: @on_rotation)
  36. secure_compare(@value, other_value) ||
  37. run_rotations(on_rotation) { |wrapper| wrapper.secure_compare!(other_value) } ||
  38. raise(InvalidMatch)
  39. end
  40. 1 private
  41. 1 def build_rotation(previous_value, _options)
  42. self.class.new(previous_value)
  43. end
  44. end
  45. end

lib/active_support/security_utils.rb

50.0% lines covered

12 relevant lines. 6 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module ActiveSupport
  3. 2 module SecurityUtils
  4. # Constant time string comparison, for fixed length strings.
  5. #
  6. # The values compared should be of fixed length, such as strings
  7. # that have already been processed by HMAC. Raises in case of length mismatch.
  8. 2 def fixed_length_secure_compare(a, b)
  9. raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
  10. l = a.unpack "C#{a.bytesize}"
  11. res = 0
  12. b.each_byte { |byte| res |= byte ^ l.shift }
  13. res == 0
  14. end
  15. 2 module_function :fixed_length_secure_compare
  16. # Secure string comparison for strings of variable length.
  17. #
  18. # While a timing attack would not be able to discern the content of
  19. # a secret compared via secure_compare, it is possible to determine
  20. # the secret length. This should be considered when using secure_compare
  21. # to compare weak, short secrets to user input.
  22. 2 def secure_compare(a, b)
  23. a.length == b.length && fixed_length_secure_compare(a, b)
  24. end
  25. 2 module_function :secure_compare
  26. end
  27. end

lib/active_support/string_inquirer.rb

60.0% lines covered

10 relevant lines. 6 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/symbol/starts_ends_with"
  3. 1 module ActiveSupport
  4. # Wrapping a string in this class gives you a prettier way to test
  5. # for equality. The value returned by <tt>Rails.env</tt> is wrapped
  6. # in a StringInquirer object, so instead of calling this:
  7. #
  8. # Rails.env == 'production'
  9. #
  10. # you can call this:
  11. #
  12. # Rails.env.production?
  13. #
  14. # == Instantiating a new StringInquirer
  15. #
  16. # vehicle = ActiveSupport::StringInquirer.new('car')
  17. # vehicle.car? # => true
  18. # vehicle.bike? # => false
  19. 1 class StringInquirer < String
  20. 1 private
  21. 1 def respond_to_missing?(method_name, include_private = false)
  22. method_name.end_with?("?") || super
  23. end
  24. 1 def method_missing(method_name, *arguments)
  25. if method_name.end_with?("?")
  26. self == method_name[0..-2]
  27. else
  28. super
  29. end
  30. end
  31. end
  32. end

lib/active_support/subscriber.rb

72.6% lines covered

73 relevant lines. 53 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/per_thread_registry"
  3. 1 require "active_support/notifications"
  4. 1 module ActiveSupport
  5. # ActiveSupport::Subscriber is an object set to consume
  6. # ActiveSupport::Notifications. The subscriber dispatches notifications to
  7. # a registered object based on its given namespace.
  8. #
  9. # An example would be an Active Record subscriber responsible for collecting
  10. # statistics about queries:
  11. #
  12. # module ActiveRecord
  13. # class StatsSubscriber < ActiveSupport::Subscriber
  14. # attach_to :active_record
  15. #
  16. # def sql(event)
  17. # Statsd.timing("sql.#{event.payload[:name]}", event.duration)
  18. # end
  19. # end
  20. # end
  21. #
  22. # After configured, whenever a "sql.active_record" notification is published,
  23. # it will properly dispatch the event (ActiveSupport::Notifications::Event) to
  24. # the +sql+ method.
  25. #
  26. # We can detach a subscriber as well:
  27. #
  28. # ActiveRecord::StatsSubscriber.detach_from(:active_record)
  29. 1 class Subscriber
  30. 1 class << self
  31. # Attach the subscriber to a namespace.
  32. 1 def attach_to(namespace, subscriber = new, notifier = ActiveSupport::Notifications)
  33. 2 @namespace = namespace
  34. 2 @subscriber = subscriber
  35. 2 @notifier = notifier
  36. 2 subscribers << subscriber
  37. # Add event subscribers for all existing methods on the class.
  38. 2 subscriber.public_methods(false).each do |event|
  39. add_event_subscriber(event)
  40. end
  41. end
  42. # Detach the subscriber from a namespace.
  43. 1 def detach_from(namespace, notifier = ActiveSupport::Notifications)
  44. 1 @namespace = namespace
  45. 1 @subscriber = find_attached_subscriber
  46. 1 @notifier = notifier
  47. 1 return unless subscriber
  48. 1 subscribers.delete(subscriber)
  49. # Remove event subscribers of all existing methods on the class.
  50. 1 subscriber.public_methods(false).each do |event|
  51. remove_event_subscriber(event)
  52. end
  53. # Reset notifier so that event subscribers will not add for new methods added to the class.
  54. 1 @notifier = nil
  55. end
  56. # Adds event subscribers for all new methods added to the class.
  57. 1 def method_added(event)
  58. # Only public methods are added as subscribers, and only if a notifier
  59. # has been set up. This means that subscribers will only be set up for
  60. # classes that call #attach_to.
  61. 28 if public_method_defined?(event) && notifier
  62. 3 add_event_subscriber(event)
  63. end
  64. end
  65. 1 def subscribers
  66. 4 @@subscribers ||= []
  67. end
  68. 1 private
  69. 1 attr_reader :subscriber, :notifier, :namespace
  70. 1 def add_event_subscriber(event) # :doc:
  71. 3 return if invalid_event?(event)
  72. 3 pattern = prepare_pattern(event)
  73. # Don't add multiple subscribers (e.g. if methods are redefined).
  74. 3 return if pattern_subscribed?(pattern)
  75. 2 subscriber.patterns[pattern] = notifier.subscribe(pattern, subscriber)
  76. end
  77. 1 def remove_event_subscriber(event) # :doc:
  78. return if invalid_event?(event)
  79. pattern = prepare_pattern(event)
  80. return unless pattern_subscribed?(pattern)
  81. notifier.unsubscribe(subscriber.patterns[pattern])
  82. subscriber.patterns.delete(pattern)
  83. end
  84. 1 def find_attached_subscriber
  85. 3 subscribers.find { |attached_subscriber| attached_subscriber.instance_of?(self) }
  86. end
  87. 1 def invalid_event?(event)
  88. 3 %i{ start finish }.include?(event.to_sym)
  89. end
  90. 1 def prepare_pattern(event)
  91. 3 "#{event}.#{namespace}"
  92. end
  93. 1 def pattern_subscribed?(pattern)
  94. 3 subscriber.patterns.key?(pattern)
  95. end
  96. end
  97. 1 attr_reader :patterns # :nodoc:
  98. 1 def initialize
  99. 2 @queue_key = [self.class.name, object_id].join "-"
  100. 2 @patterns = {}
  101. 2 super
  102. end
  103. 1 def start(name, id, payload)
  104. event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload)
  105. event.start!
  106. parent = event_stack.last
  107. parent << event if parent
  108. event_stack.push event
  109. end
  110. 1 def finish(name, id, payload)
  111. event = event_stack.pop
  112. event.finish!
  113. event.payload.merge!(payload)
  114. method = name.split(".").first
  115. send(method, event)
  116. end
  117. 1 private
  118. 1 def event_stack
  119. SubscriberQueueRegistry.instance.get_queue(@queue_key)
  120. end
  121. end
  122. # This is a registry for all the event stacks kept for subscribers.
  123. #
  124. # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
  125. # for further details.
  126. 1 class SubscriberQueueRegistry # :nodoc:
  127. 1 extend PerThreadRegistry
  128. 1 def initialize
  129. @registry = {}
  130. end
  131. 1 def get_queue(queue_key)
  132. @registry[queue_key] ||= []
  133. end
  134. end
  135. end

lib/active_support/tagged_logging.rb

39.62% lines covered

53 relevant lines. 21 lines covered and 32 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/delegation"
  3. 1 require "active_support/core_ext/object/blank"
  4. 1 require "logger"
  5. 1 require "active_support/logger"
  6. 1 module ActiveSupport
  7. # Wraps any standard Logger object to provide tagging capabilities.
  8. #
  9. # May be called with a block:
  10. #
  11. # logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
  12. # logger.tagged('BCX') { logger.info 'Stuff' } # Logs "[BCX] Stuff"
  13. # logger.tagged('BCX', "Jason") { logger.info 'Stuff' } # Logs "[BCX] [Jason] Stuff"
  14. # logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff"
  15. #
  16. # If called without a block, a new logger will be returned with applied tags:
  17. #
  18. # logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
  19. # logger.tagged("BCX").info "Stuff" # Logs "[BCX] Stuff"
  20. # logger.tagged("BCX", "Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff"
  21. # logger.tagged("BCX").tagged("Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff"
  22. #
  23. # This is used by the default Rails.logger as configured by Railties to make
  24. # it easy to stamp log lines with subdomains, request ids, and anything else
  25. # to aid debugging of multi-user production applications.
  26. 1 module TaggedLogging
  27. 1 module Formatter # :nodoc:
  28. # This method is invoked when a log event occurs.
  29. 1 def call(severity, timestamp, progname, msg)
  30. super(severity, timestamp, progname, "#{tags_text}#{msg}")
  31. end
  32. 1 def tagged(*tags)
  33. new_tags = push_tags(*tags)
  34. yield self
  35. ensure
  36. pop_tags(new_tags.size)
  37. end
  38. 1 def push_tags(*tags)
  39. tags.flatten!
  40. tags.reject!(&:blank?)
  41. current_tags.concat tags
  42. tags
  43. end
  44. 1 def pop_tags(size = 1)
  45. current_tags.pop size
  46. end
  47. 1 def clear_tags!
  48. current_tags.clear
  49. end
  50. 1 def current_tags
  51. # We use our object ID here to avoid conflicting with other instances
  52. thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}"
  53. Thread.current[thread_key] ||= []
  54. end
  55. 1 def tags_text
  56. tags = current_tags
  57. if tags.one?
  58. "[#{tags[0]}] "
  59. elsif tags.any?
  60. tags.collect { |tag| "[#{tag}] " }.join
  61. end
  62. end
  63. end
  64. 1 module LocalTagStorage # :nodoc:
  65. 1 attr_accessor :current_tags
  66. 1 def self.extended(base)
  67. base.current_tags = []
  68. end
  69. end
  70. 1 def self.new(logger)
  71. logger = logger.dup
  72. if logger.formatter
  73. logger.formatter = logger.formatter.dup
  74. else
  75. # Ensure we set a default formatter so we aren't extending nil!
  76. logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
  77. end
  78. logger.formatter.extend Formatter
  79. logger.extend(self)
  80. end
  81. 1 delegate :push_tags, :pop_tags, :clear_tags!, to: :formatter
  82. 1 def tagged(*tags)
  83. if block_given?
  84. formatter.tagged(*tags) { yield self }
  85. else
  86. logger = ActiveSupport::TaggedLogging.new(self)
  87. logger.formatter.extend LocalTagStorage
  88. logger.push_tags(*formatter.current_tags, *tags)
  89. logger
  90. end
  91. end
  92. 1 def flush
  93. clear_tags!
  94. super if defined?(super)
  95. end
  96. end
  97. end

lib/active_support/test_case.rb

88.52% lines covered

61 relevant lines. 54 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 gem "minitest" # make sure we get the gem, not stdlib
  3. 23 require "minitest"
  4. 23 require "active_support/testing/tagged_logging"
  5. 23 require "active_support/testing/setup_and_teardown"
  6. 23 require "active_support/testing/assertions"
  7. 23 require "active_support/testing/deprecation"
  8. 23 require "active_support/testing/declarative"
  9. 23 require "active_support/testing/isolation"
  10. 23 require "active_support/testing/constant_lookup"
  11. 23 require "active_support/testing/time_helpers"
  12. 23 require "active_support/testing/file_fixtures"
  13. 23 require "active_support/testing/parallelization"
  14. 23 require "concurrent/utility/processor_counter"
  15. 23 module ActiveSupport
  16. 23 class TestCase < ::Minitest::Test
  17. 23 Assertion = Minitest::Assertion
  18. 23 class << self
  19. # Sets the order in which test cases are run.
  20. #
  21. # ActiveSupport::TestCase.test_order = :random # => :random
  22. #
  23. # Valid values are:
  24. # * +:random+ (to run tests in random order)
  25. # * +:parallel+ (to run tests in parallel)
  26. # * +:sorted+ (to run tests alphabetically by method name)
  27. # * +:alpha+ (equivalent to +:sorted+)
  28. 23 def test_order=(new_order)
  29. ActiveSupport.test_order = new_order
  30. end
  31. # Returns the order in which test cases are run.
  32. #
  33. # ActiveSupport::TestCase.test_order # => :random
  34. #
  35. # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
  36. # Defaults to +:random+.
  37. 23 def test_order
  38. 898 ActiveSupport.test_order ||= :random
  39. end
  40. # Parallelizes the test suite.
  41. #
  42. # Takes a +workers+ argument that controls how many times the process
  43. # is forked. For each process a new database will be created suffixed
  44. # with the worker number.
  45. #
  46. # test-database-0
  47. # test-database-1
  48. #
  49. # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
  50. # and the environment variable will be used instead. This is useful for CI
  51. # environments, or other environments where you may need more workers than
  52. # you do for local testing.
  53. #
  54. # If the number of workers is set to +1+ or fewer, the tests will not be
  55. # parallelized.
  56. #
  57. # If +workers+ is set to +:number_of_processors+, the number of workers will be
  58. # set to the actual core count on the machine you are on.
  59. #
  60. # The default parallelization method is to fork processes. If you'd like to
  61. # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
  62. # method. Note the threaded parallelization does not create multiple
  63. # database and will not work with system tests at this time.
  64. #
  65. # parallelize(workers: :number_of_processors, with: :threads)
  66. #
  67. # The threaded parallelization uses minitest's parallel executor directly.
  68. # The processes parallelization uses a Ruby DRb server.
  69. 23 def parallelize(workers: :number_of_processors, with: :processes)
  70. 23 workers = Concurrent.physical_processor_count if workers == :number_of_processors
  71. 23 workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
  72. 23 return if workers <= 1
  73. 23 executor = case with
  74. when :processes
  75. 23 Testing::Parallelization.new(workers)
  76. when :threads
  77. Minitest::Parallel::Executor.new(workers)
  78. else
  79. raise ArgumentError, "#{with} is not a supported parallelization executor."
  80. end
  81. 23 self.lock_threads = false if defined?(self.lock_threads) && with == :threads
  82. 23 Minitest.parallel_executor = executor
  83. 23 parallelize_me!
  84. end
  85. # Set up hook for parallel testing. This can be used if you have multiple
  86. # databases or any behavior that needs to be run after the process is forked
  87. # but before the tests run.
  88. #
  89. # Note: this feature is not available with the threaded parallelization.
  90. #
  91. # In your +test_helper.rb+ add the following:
  92. #
  93. # class ActiveSupport::TestCase
  94. # parallelize_setup do
  95. # # create databases
  96. # end
  97. # end
  98. 23 def parallelize_setup(&block)
  99. ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
  100. yield worker
  101. end
  102. end
  103. # Clean up hook for parallel testing. This can be used to drop databases
  104. # if your app uses multiple write/read databases or other clean up before
  105. # the tests finish. This runs before the forked process is closed.
  106. #
  107. # Note: this feature is not available with the threaded parallelization.
  108. #
  109. # In your +test_helper.rb+ add the following:
  110. #
  111. # class ActiveSupport::TestCase
  112. # parallelize_teardown do
  113. # # drop databases
  114. # end
  115. # end
  116. 23 def parallelize_teardown(&block)
  117. ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
  118. yield worker
  119. end
  120. end
  121. end
  122. 23 alias_method :method_name, :name
  123. 23 include ActiveSupport::Testing::TaggedLogging
  124. 23 prepend ActiveSupport::Testing::SetupAndTeardown
  125. 23 include ActiveSupport::Testing::Assertions
  126. 23 include ActiveSupport::Testing::Deprecation
  127. 23 include ActiveSupport::Testing::TimeHelpers
  128. 23 include ActiveSupport::Testing::FileFixtures
  129. 23 extend ActiveSupport::Testing::Declarative
  130. # test/unit backwards compatibility methods
  131. 23 alias :assert_raise :assert_raises
  132. 23 alias :assert_not_empty :refute_empty
  133. 23 alias :assert_not_equal :refute_equal
  134. 23 alias :assert_not_in_delta :refute_in_delta
  135. 23 alias :assert_not_in_epsilon :refute_in_epsilon
  136. 23 alias :assert_not_includes :refute_includes
  137. 23 alias :assert_not_instance_of :refute_instance_of
  138. 23 alias :assert_not_kind_of :refute_kind_of
  139. 23 alias :assert_no_match :refute_match
  140. 23 alias :assert_not_nil :refute_nil
  141. 23 alias :assert_not_operator :refute_operator
  142. 23 alias :assert_not_predicate :refute_predicate
  143. 23 alias :assert_not_respond_to :refute_respond_to
  144. 23 alias :assert_not_same :refute_same
  145. 23 ActiveSupport.run_load_hooks(:active_support_test_case, self)
  146. end
  147. end

lib/active_support/testing/assertions.rb

18.64% lines covered

59 relevant lines. 11 lines covered and 48 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/enumerable"
  3. 23 module ActiveSupport
  4. 23 module Testing
  5. 23 module Assertions
  6. 23 UNTRACKED = Object.new # :nodoc:
  7. # Asserts that an expression is not truthy. Passes if <tt>object</tt> is
  8. # +nil+ or +false+. "Truthy" means "considered true in a conditional"
  9. # like <tt>if foo</tt>.
  10. #
  11. # assert_not nil # => true
  12. # assert_not false # => true
  13. # assert_not 'foo' # => Expected "foo" to be nil or false
  14. #
  15. # An error message can be specified.
  16. #
  17. # assert_not foo, 'foo should be false'
  18. 23 def assert_not(object, message = nil)
  19. message ||= "Expected #{mu_pp(object)} to be nil or false"
  20. assert !object, message
  21. end
  22. # Assertion that the block should not raise an exception.
  23. #
  24. # Passes if evaluated code in the yielded block raises no exception.
  25. #
  26. # assert_nothing_raised do
  27. # perform_service(param: 'no_exception')
  28. # end
  29. 23 def assert_nothing_raised
  30. yield
  31. rescue => error
  32. raise Minitest::UnexpectedError.new(error)
  33. end
  34. # Test numeric difference between the return value of an expression as a
  35. # result of what is evaluated in the yielded block.
  36. #
  37. # assert_difference 'Article.count' do
  38. # post :create, params: { article: {...} }
  39. # end
  40. #
  41. # An arbitrary expression is passed in and evaluated.
  42. #
  43. # assert_difference 'Article.last.comments(:reload).size' do
  44. # post :create, params: { comment: {...} }
  45. # end
  46. #
  47. # An arbitrary positive or negative difference can be specified.
  48. # The default is <tt>1</tt>.
  49. #
  50. # assert_difference 'Article.count', -1 do
  51. # post :delete, params: { id: ... }
  52. # end
  53. #
  54. # An array of expressions can also be passed in and evaluated.
  55. #
  56. # assert_difference [ 'Article.count', 'Post.count' ], 2 do
  57. # post :create, params: { article: {...} }
  58. # end
  59. #
  60. # A hash of expressions/numeric differences can also be passed in and evaluated.
  61. #
  62. # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
  63. # post :create, params: { article: {...} }
  64. # end
  65. #
  66. # A lambda or a list of lambdas can be passed in and evaluated:
  67. #
  68. # assert_difference ->{ Article.count }, 2 do
  69. # post :create, params: { article: {...} }
  70. # end
  71. #
  72. # assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
  73. # post :create, params: { article: {...} }
  74. # end
  75. #
  76. # An error message can be specified.
  77. #
  78. # assert_difference 'Article.count', -1, 'An Article should be destroyed' do
  79. # post :delete, params: { id: ... }
  80. # end
  81. 23 def assert_difference(expression, *args, &block)
  82. expressions =
  83. if expression.is_a?(Hash)
  84. message = args[0]
  85. expression
  86. else
  87. difference = args[0] || 1
  88. message = args[1]
  89. Array(expression).index_with(difference)
  90. end
  91. exps = expressions.keys.map { |e|
  92. e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
  93. }
  94. before = exps.map(&:call)
  95. retval = assert_nothing_raised(&block)
  96. expressions.zip(exps, before) do |(code, diff), exp, before_value|
  97. error = "#{code.inspect} didn't change by #{diff}"
  98. error = "#{message}.\n#{error}" if message
  99. assert_equal(before_value + diff, exp.call, error)
  100. end
  101. retval
  102. end
  103. # Assertion that the numeric result of evaluating an expression is not
  104. # changed before and after invoking the passed in block.
  105. #
  106. # assert_no_difference 'Article.count' do
  107. # post :create, params: { article: invalid_attributes }
  108. # end
  109. #
  110. # A lambda can be passed in and evaluated.
  111. #
  112. # assert_no_difference -> { Article.count } do
  113. # post :create, params: { article: invalid_attributes }
  114. # end
  115. #
  116. # An error message can be specified.
  117. #
  118. # assert_no_difference 'Article.count', 'An Article should not be created' do
  119. # post :create, params: { article: invalid_attributes }
  120. # end
  121. #
  122. # An array of expressions can also be passed in and evaluated.
  123. #
  124. # assert_no_difference [ 'Article.count', -> { Post.count } ] do
  125. # post :create, params: { article: invalid_attributes }
  126. # end
  127. 23 def assert_no_difference(expression, message = nil, &block)
  128. assert_difference expression, 0, message, &block
  129. end
  130. # Assertion that the result of evaluating an expression is changed before
  131. # and after invoking the passed in block.
  132. #
  133. # assert_changes 'Status.all_good?' do
  134. # post :create, params: { status: { ok: false } }
  135. # end
  136. #
  137. # You can pass the block as a string to be evaluated in the context of
  138. # the block. A lambda can be passed for the block as well.
  139. #
  140. # assert_changes -> { Status.all_good? } do
  141. # post :create, params: { status: { ok: false } }
  142. # end
  143. #
  144. # The assertion is useful to test side effects. The passed block can be
  145. # anything that can be converted to string with #to_s.
  146. #
  147. # assert_changes :@object do
  148. # @object = 42
  149. # end
  150. #
  151. # The keyword arguments :from and :to can be given to specify the
  152. # expected initial value and the expected value after the block was
  153. # executed.
  154. #
  155. # assert_changes :@object, from: nil, to: :foo do
  156. # @object = :foo
  157. # end
  158. #
  159. # An error message can be specified.
  160. #
  161. # assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
  162. # post :create, params: { status: { incident: true } }
  163. # end
  164. 23 def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
  165. exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
  166. before = exp.call
  167. retval = assert_nothing_raised(&block)
  168. unless from == UNTRACKED
  169. error = "Expected change from #{from.inspect}"
  170. error = "#{message}.\n#{error}" if message
  171. assert from === before, error
  172. end
  173. after = exp.call
  174. error = "#{expression.inspect} didn't change"
  175. error = "#{error}. It was already #{to}" if before == to
  176. error = "#{message}.\n#{error}" if message
  177. assert_not_equal before, after, error
  178. unless to == UNTRACKED
  179. error = "Expected change to #{to}\n"
  180. error = "#{message}.\n#{error}" if message
  181. assert to === after, error
  182. end
  183. retval
  184. end
  185. # Assertion that the result of evaluating an expression is not changed before
  186. # and after invoking the passed in block.
  187. #
  188. # assert_no_changes 'Status.all_good?' do
  189. # post :create, params: { status: { ok: true } }
  190. # end
  191. #
  192. # An error message can be specified.
  193. #
  194. # assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
  195. # post :create, params: { status: { ok: false } }
  196. # end
  197. 23 def assert_no_changes(expression, message = nil, &block)
  198. exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
  199. before = exp.call
  200. retval = assert_nothing_raised(&block)
  201. after = exp.call
  202. error = "#{expression.inspect} changed"
  203. error = "#{message}.\n#{error}" if message
  204. if before.nil?
  205. assert_nil after, error
  206. else
  207. assert_equal before, after, error
  208. end
  209. retval
  210. end
  211. end
  212. end
  213. end

lib/active_support/testing/autorun.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 gem "minitest"
  3. 24 require "minitest"
  4. 24 Minitest.autorun

lib/active_support/testing/constant_lookup.rb

53.33% lines covered

15 relevant lines. 8 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/concern"
  3. 23 require "active_support/inflector"
  4. 23 module ActiveSupport
  5. 23 module Testing
  6. # Resolves a constant from a minitest spec name.
  7. #
  8. # Given the following spec-style test:
  9. #
  10. # describe WidgetsController, :index do
  11. # describe "authenticated user" do
  12. # describe "returns widgets" do
  13. # it "has a controller that exists" do
  14. # assert_kind_of WidgetsController, @controller
  15. # end
  16. # end
  17. # end
  18. # end
  19. #
  20. # The test will have the following name:
  21. #
  22. # "WidgetsController::index::authenticated user::returns widgets"
  23. #
  24. # The constant WidgetsController can be resolved from the name.
  25. # The following code will resolve the constant:
  26. #
  27. # controller = determine_constant_from_test_name(name) do |constant|
  28. # Class === constant && constant < ::ActionController::Metal
  29. # end
  30. 23 module ConstantLookup
  31. 23 extend ::ActiveSupport::Concern
  32. 23 module ClassMethods # :nodoc:
  33. 23 def determine_constant_from_test_name(test_name)
  34. names = test_name.split "::"
  35. while names.size > 0 do
  36. names.last.sub!(/Test$/, "")
  37. begin
  38. constant = names.join("::").safe_constantize
  39. break(constant) if yield(constant)
  40. ensure
  41. names.pop
  42. end
  43. end
  44. end
  45. end
  46. end
  47. end
  48. end

lib/active_support/testing/declarative.rb

83.33% lines covered

12 relevant lines. 10 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport
  3. 23 module Testing
  4. 23 module Declarative
  5. 23 unless defined?(Spec)
  6. # Helper to define a test method using a String. Under the hood, it replaces
  7. # spaces with underscores and defines the test method.
  8. #
  9. # test "verify something" do
  10. # ...
  11. # end
  12. 23 def test(name, &block)
  13. 526 test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
  14. 526 defined = method_defined? test_name
  15. 526 raise "#{test_name} is already defined in #{self}" if defined
  16. 526 if block_given?
  17. 526 define_method(test_name, &block)
  18. else
  19. define_method(test_name) do
  20. flunk "No implementation provided for #{name}"
  21. end
  22. end
  23. end
  24. end
  25. end
  26. end
  27. end

lib/active_support/testing/deprecation.rb

29.17% lines covered

24 relevant lines. 7 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/deprecation"
  3. 23 module ActiveSupport
  4. 23 module Testing
  5. 23 module Deprecation #:nodoc:
  6. 23 def assert_deprecated(match = nil, deprecator = nil, &block)
  7. result, warnings = collect_deprecations(deprecator, &block)
  8. assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
  9. if match
  10. match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
  11. assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
  12. end
  13. result
  14. end
  15. 23 def assert_not_deprecated(deprecator = nil, &block)
  16. result, deprecations = collect_deprecations(deprecator, &block)
  17. assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
  18. result
  19. end
  20. 23 def collect_deprecations(deprecator = nil)
  21. deprecator ||= ActiveSupport::Deprecation
  22. old_behavior = deprecator.behavior
  23. deprecations = []
  24. deprecator.behavior = Proc.new do |message, callstack|
  25. deprecations << message
  26. end
  27. result = yield
  28. [result, deprecations]
  29. ensure
  30. deprecator.behavior = old_behavior
  31. end
  32. end
  33. end
  34. end

lib/active_support/testing/file_fixtures.rb

61.54% lines covered

13 relevant lines. 8 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/concern"
  3. 23 module ActiveSupport
  4. 23 module Testing
  5. # Adds simple access to sample files called file fixtures.
  6. # File fixtures are normal files stored in
  7. # <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
  8. #
  9. # File fixtures are represented as +Pathname+ objects.
  10. # This makes it easy to extract specific information:
  11. #
  12. # file_fixture("example.txt").read # get the file's content
  13. # file_fixture("example.mp3").size # get the file size
  14. 23 module FileFixtures
  15. 23 extend ActiveSupport::Concern
  16. 23 included do
  17. 23 class_attribute :file_fixture_path, instance_writer: false
  18. end
  19. # Returns a +Pathname+ to the fixture file named +fixture_name+.
  20. #
  21. # Raises +ArgumentError+ if +fixture_name+ can't be found.
  22. 23 def file_fixture(fixture_name)
  23. path = Pathname.new(File.join(file_fixture_path, fixture_name))
  24. if path.exist?
  25. path
  26. else
  27. msg = "the directory '%s' does not contain a file named '%s'"
  28. raise ArgumentError, msg % [file_fixture_path, fixture_name]
  29. end
  30. end
  31. end
  32. end
  33. end

lib/active_support/testing/isolation.rb

26.67% lines covered

60 relevant lines. 16 lines covered and 44 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport
  3. 23 module Testing
  4. 23 module Isolation
  5. 23 require "thread"
  6. 23 def self.included(klass) #:nodoc:
  7. 4 klass.class_eval do
  8. 4 parallelize_me!
  9. end
  10. end
  11. 23 def self.forking_env?
  12. 23 !ENV["NO_FORK"] && Process.respond_to?(:fork)
  13. end
  14. 23 def run
  15. serialized = run_in_isolation do
  16. super
  17. end
  18. Marshal.load(serialized)
  19. end
  20. 23 module Forking
  21. 23 def run_in_isolation(&blk)
  22. read, write = IO.pipe
  23. read.binmode
  24. write.binmode
  25. pid = fork do
  26. read.close
  27. yield
  28. begin
  29. if error?
  30. failures.map! { |e|
  31. begin
  32. Marshal.dump e
  33. e
  34. rescue TypeError
  35. ex = Exception.new e.message
  36. ex.set_backtrace e.backtrace
  37. Minitest::UnexpectedError.new ex
  38. end
  39. }
  40. end
  41. test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
  42. result = Marshal.dump(test_result)
  43. end
  44. write.puts [result].pack("m")
  45. exit!
  46. end
  47. write.close
  48. result = read.read
  49. Process.wait2(pid)
  50. result.unpack1("m")
  51. end
  52. end
  53. 23 module Subprocess
  54. 23 ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
  55. # Crazy H4X to get this working in windows / jruby with
  56. # no forking.
  57. 23 def run_in_isolation(&blk)
  58. require "tempfile"
  59. if ENV["ISOLATION_TEST"]
  60. yield
  61. test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
  62. File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
  63. file.puts [Marshal.dump(test_result)].pack("m")
  64. end
  65. exit!
  66. else
  67. Tempfile.open("isolation") do |tmpfile|
  68. env = {
  69. "ISOLATION_TEST" => self.class.name,
  70. "ISOLATION_OUTPUT" => tmpfile.path
  71. }
  72. test_opts = "-n#{self.class.name}##{name}"
  73. load_path_args = []
  74. $-I.each do |p|
  75. load_path_args << "-I"
  76. load_path_args << File.expand_path(p)
  77. end
  78. child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
  79. begin
  80. Process.wait(child.pid)
  81. rescue Errno::ECHILD # The child process may exit before we wait
  82. nil
  83. end
  84. return tmpfile.read.unpack1("m")
  85. end
  86. end
  87. end
  88. end
  89. 23 include forking_env? ? Forking : Subprocess
  90. end
  91. end
  92. end

lib/active_support/testing/method_call_assertions.rb

28.95% lines covered

38 relevant lines. 11 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require "minitest/mock"
  3. 24 module ActiveSupport
  4. 24 module Testing
  5. 24 module MethodCallAssertions # :nodoc:
  6. 24 private
  7. 24 def assert_called(object, method_name, message = nil, times: 1, returns: nil)
  8. times_called = 0
  9. object.stub(method_name, proc { times_called += 1; returns }) { yield }
  10. error = "Expected #{method_name} to be called #{times} times, " \
  11. "but was called #{times_called} times"
  12. error = "#{message}.\n#{error}" if message
  13. assert_equal times, times_called, error
  14. end
  15. 24 def assert_called_with(object, method_name, args, returns: nil)
  16. mock = Minitest::Mock.new
  17. if args.all? { |arg| arg.is_a?(Array) }
  18. args.each { |arg| mock.expect(:call, returns, arg) }
  19. else
  20. mock.expect(:call, returns, args)
  21. end
  22. object.stub(method_name, mock) { yield }
  23. mock.verify
  24. end
  25. 24 def assert_not_called(object, method_name, message = nil, &block)
  26. assert_called(object, method_name, message, times: 0, &block)
  27. end
  28. 24 def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)
  29. times_called = 0
  30. klass.define_method("stubbed_#{method_name}") do |*|
  31. times_called += 1
  32. returns
  33. end
  34. klass.alias_method "original_#{method_name}", method_name
  35. klass.alias_method method_name, "stubbed_#{method_name}"
  36. yield
  37. error = "Expected #{method_name} to be called #{times} times, but was called #{times_called} times"
  38. error = "#{message}.\n#{error}" if message
  39. assert_equal times, times_called, error
  40. ensure
  41. klass.alias_method method_name, "original_#{method_name}"
  42. klass.undef_method "original_#{method_name}"
  43. klass.undef_method "stubbed_#{method_name}"
  44. end
  45. 24 def assert_not_called_on_instance_of(klass, method_name, message = nil, &block)
  46. assert_called_on_instance_of(klass, method_name, message, times: 0, &block)
  47. end
  48. 24 def stub_any_instance(klass, instance: klass.new)
  49. klass.stub(:new, instance) { yield instance }
  50. end
  51. end
  52. end
  53. end

lib/active_support/testing/parallelization.rb

93.1% lines covered

29 relevant lines. 27 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "drb"
  3. 23 require "drb/unix" unless Gem.win_platform?
  4. 23 require "active_support/core_ext/module/attribute_accessors"
  5. 23 require "active_support/testing/parallelization/server"
  6. 23 require "active_support/testing/parallelization/worker"
  7. 23 module ActiveSupport
  8. 23 module Testing
  9. 23 class Parallelization # :nodoc:
  10. 23 @@after_fork_hooks = []
  11. 23 def self.after_fork_hook(&blk)
  12. @@after_fork_hooks << blk
  13. end
  14. 23 cattr_reader :after_fork_hooks
  15. 23 @@run_cleanup_hooks = []
  16. 23 def self.run_cleanup_hook(&blk)
  17. @@run_cleanup_hooks << blk
  18. end
  19. 23 cattr_reader :run_cleanup_hooks
  20. 23 def initialize(worker_count)
  21. 23 @worker_count = worker_count
  22. 23 @queue_server = Server.new
  23. 23 @worker_pool = []
  24. 23 @url = DRb.start_service("drbunix:", @queue_server).uri
  25. end
  26. 23 def start
  27. 23 @worker_pool = @worker_count.times.map do |worker|
  28. 46 Worker.new(worker, @url).start
  29. end
  30. end
  31. 23 def <<(work)
  32. 5804 @queue_server << work
  33. end
  34. 23 def shutdown
  35. 23 @queue_server.shutdown
  36. 69 @worker_pool.each { |pid| Process.waitpid pid }
  37. end
  38. end
  39. end
  40. end

lib/active_support/testing/parallelization/server.rb

85.71% lines covered

42 relevant lines. 36 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "drb"
  3. 23 require "drb/unix" unless Gem.win_platform?
  4. 23 module ActiveSupport
  5. 23 module Testing
  6. 23 class Parallelization # :nodoc:
  7. 23 class Server
  8. 23 include DRb::DRbUndumped
  9. 23 def initialize
  10. 23 @queue = Queue.new
  11. 23 @active_workers = Concurrent::Map.new
  12. 23 @in_flight = Concurrent::Map.new
  13. end
  14. 23 def record(reporter, result)
  15. 5804 raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown)
  16. 5804 @in_flight.delete([result.klass, result.name])
  17. 5804 reporter.synchronize do
  18. 5804 reporter.record(result)
  19. end
  20. end
  21. 23 def <<(o)
  22. 5804 o[2] = DRbObject.new(o[2]) if o
  23. 5804 @queue << o
  24. end
  25. 23 def pop
  26. 5850 if test = @queue.pop
  27. 5804 @in_flight[[test[0].to_s, test[1]]] = test
  28. 5804 test
  29. end
  30. end
  31. 23 def start_worker(worker_id)
  32. 46 @active_workers[worker_id] = true
  33. end
  34. 23 def stop_worker(worker_id)
  35. 46 @active_workers.delete(worker_id)
  36. end
  37. 23 def active_workers?
  38. 91 @active_workers.size > 0
  39. end
  40. 23 def shutdown
  41. # Wait for initial queue to drain
  42. 23 while @queue.length != 0
  43. 885 sleep 0.1
  44. end
  45. 23 @queue.close
  46. # Wait until all workers have finished
  47. 23 while active_workers?
  48. 68 sleep 0.1
  49. end
  50. 23 @in_flight.values.each do |(klass, name, reporter)|
  51. result = Minitest::Result.from(klass.new(name))
  52. error = RuntimeError.new("result not reported")
  53. error.set_backtrace([""])
  54. result.failures << Minitest::UnexpectedError.new(error)
  55. reporter.synchronize do
  56. reporter.record(result)
  57. end
  58. end
  59. end
  60. end
  61. end
  62. end
  63. end

lib/active_support/testing/parallelization/worker.rb

35.19% lines covered

54 relevant lines. 19 lines covered and 35 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport
  3. 23 module Testing
  4. 23 class Parallelization # :nodoc:
  5. 23 class Worker
  6. 23 def initialize(number, url)
  7. 46 @id = SecureRandom.uuid
  8. 46 @number = number
  9. 46 @url = url
  10. 46 @setup_exception = nil
  11. end
  12. 23 def start
  13. 46 fork do
  14. set_process_title("(starting)")
  15. DRb.stop_service
  16. @queue = DRbObject.new_with_uri(@url)
  17. @queue.start_worker(@id)
  18. begin
  19. after_fork
  20. rescue => @setup_exception; end
  21. work_from_queue
  22. ensure
  23. set_process_title("(stopping)")
  24. run_cleanup
  25. @queue.stop_worker(@id)
  26. end
  27. end
  28. 23 def work_from_queue
  29. while job = @queue.pop
  30. perform_job(job)
  31. end
  32. end
  33. 23 def perform_job(job)
  34. klass = job[0]
  35. method = job[1]
  36. reporter = job[2]
  37. set_process_title("#{klass}##{method}")
  38. result = klass.with_info_handler reporter do
  39. Minitest.run_one_method(klass, method)
  40. end
  41. safe_record(reporter, result)
  42. end
  43. 23 def safe_record(reporter, result)
  44. add_setup_exception(result) if @setup_exception
  45. begin
  46. @queue.record(reporter, result)
  47. rescue DRb::DRbConnError
  48. result.failures.map! do |failure|
  49. if failure.respond_to?(:error)
  50. # minitest >5.14.0
  51. error = DRb::DRbRemoteError.new(failure.error)
  52. else
  53. error = DRb::DRbRemoteError.new(failure.exception)
  54. end
  55. Minitest::UnexpectedError.new(error)
  56. end
  57. @queue.record(reporter, result)
  58. end
  59. set_process_title("(idle)")
  60. end
  61. 23 def after_fork
  62. Parallelization.after_fork_hooks.each do |cb|
  63. cb.call(@number)
  64. end
  65. end
  66. 23 def run_cleanup
  67. Parallelization.run_cleanup_hooks.each do |cb|
  68. cb.call(@number)
  69. end
  70. end
  71. 23 private
  72. 23 def add_setup_exception(result)
  73. result.failures.prepend Minitest::UnexpectedError.new(@setup_exception)
  74. end
  75. 23 def set_process_title(status)
  76. Process.setproctitle("Rails test worker #{@number} - #{status}")
  77. end
  78. end
  79. end
  80. end
  81. end

lib/active_support/testing/setup_and_teardown.rb

71.43% lines covered

21 relevant lines. 15 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/callbacks"
  3. 23 module ActiveSupport
  4. 23 module Testing
  5. # Adds support for +setup+ and +teardown+ callbacks.
  6. # These callbacks serve as a replacement to overwriting the
  7. # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
  8. #
  9. # class ExampleTest < ActiveSupport::TestCase
  10. # setup do
  11. # # ...
  12. # end
  13. #
  14. # teardown do
  15. # # ...
  16. # end
  17. # end
  18. 23 module SetupAndTeardown
  19. 23 def self.prepended(klass)
  20. 23 klass.include ActiveSupport::Callbacks
  21. 23 klass.define_callbacks :setup, :teardown
  22. 23 klass.extend ClassMethods
  23. end
  24. 23 module ClassMethods
  25. # Add a callback, which runs before <tt>TestCase#setup</tt>.
  26. 23 def setup(*args, &block)
  27. 25 set_callback(:setup, :before, *args, &block)
  28. end
  29. # Add a callback, which runs after <tt>TestCase#teardown</tt>.
  30. 23 def teardown(*args, &block)
  31. 14 set_callback(:teardown, :after, *args, &block)
  32. end
  33. end
  34. 23 def before_setup # :nodoc:
  35. super
  36. run_callbacks :setup
  37. end
  38. 23 def after_teardown # :nodoc:
  39. begin
  40. run_callbacks :teardown
  41. rescue => e
  42. self.failures << Minitest::UnexpectedError.new(e)
  43. end
  44. super
  45. end
  46. end
  47. end
  48. end

lib/active_support/testing/stream.rb

25.93% lines covered

27 relevant lines. 7 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveSupport
  3. 1 module Testing
  4. 1 module Stream #:nodoc:
  5. 1 private
  6. 1 def silence_stream(stream)
  7. old_stream = stream.dup
  8. stream.reopen(IO::NULL)
  9. stream.sync = true
  10. yield
  11. ensure
  12. stream.reopen(old_stream)
  13. old_stream.close
  14. end
  15. 1 def quietly
  16. silence_stream(STDOUT) do
  17. silence_stream(STDERR) do
  18. yield
  19. end
  20. end
  21. end
  22. 1 def capture(stream)
  23. stream = stream.to_s
  24. captured_stream = Tempfile.new(stream)
  25. stream_io = eval("$#{stream}")
  26. origin_stream = stream_io.dup
  27. stream_io.reopen(captured_stream)
  28. yield
  29. stream_io.rewind
  30. captured_stream.read
  31. ensure
  32. captured_stream.close
  33. captured_stream.unlink
  34. stream_io.reopen(origin_stream)
  35. end
  36. end
  37. end
  38. end

lib/active_support/testing/tagged_logging.rb

46.67% lines covered

15 relevant lines. 7 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 module ActiveSupport
  3. 23 module Testing
  4. # Logs a "PostsControllerTest: test name" heading before each test to
  5. # make test.log easier to search and follow along with.
  6. 23 module TaggedLogging #:nodoc:
  7. 23 attr_writer :tagged_logger
  8. 23 def before_setup
  9. if tagged_logger && tagged_logger.info?
  10. heading = "#{self.class}: #{name}"
  11. divider = "-" * heading.size
  12. tagged_logger.info divider
  13. tagged_logger.info heading
  14. tagged_logger.info divider
  15. end
  16. super
  17. end
  18. 23 private
  19. 23 def tagged_logger
  20. @tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
  21. end
  22. end
  23. end
  24. end

lib/active_support/testing/time_helpers.rb

37.1% lines covered

62 relevant lines. 23 lines covered and 39 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/module/redefine_method"
  3. 23 require "active_support/core_ext/time/calculations"
  4. 23 require "concurrent/map"
  5. 23 module ActiveSupport
  6. 23 module Testing
  7. 23 class SimpleStubs # :nodoc:
  8. 23 Stub = Struct.new(:object, :method_name, :original_method)
  9. 23 def initialize
  10. @stubs = Concurrent::Map.new { |h, k| h[k] = {} }
  11. end
  12. 23 def stub_object(object, method_name, &block)
  13. if stub = stubbing(object, method_name)
  14. unstub_object(stub)
  15. end
  16. new_name = "__simple_stub__#{method_name}"
  17. @stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
  18. object.singleton_class.alias_method new_name, method_name
  19. object.define_singleton_method(method_name, &block)
  20. end
  21. 23 def unstub_all!
  22. @stubs.each_value do |object_stubs|
  23. object_stubs.each_value do |stub|
  24. unstub_object(stub)
  25. end
  26. end
  27. @stubs.clear
  28. end
  29. 23 def stubbing(object, method_name)
  30. @stubs[object.object_id][method_name]
  31. end
  32. 23 def stubbed?
  33. !@stubs.empty?
  34. end
  35. 23 private
  36. 23 def unstub_object(stub)
  37. singleton_class = stub.object.singleton_class
  38. singleton_class.silence_redefinition_of_method stub.method_name
  39. singleton_class.alias_method stub.method_name, stub.original_method
  40. singleton_class.undef_method stub.original_method
  41. end
  42. end
  43. # Contains helpers that help you test passage of time.
  44. 23 module TimeHelpers
  45. 23 def after_teardown
  46. travel_back
  47. super
  48. end
  49. # Changes current time to the time in the future or in the past by a given time difference by
  50. # stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
  51. # at the end of the test.
  52. #
  53. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  54. # travel 1.day
  55. # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
  56. # Date.current # => Sun, 10 Nov 2013
  57. # DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500
  58. #
  59. # This method also accepts a block, which will return the current time back to its original
  60. # state at the end of the block:
  61. #
  62. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  63. # travel 1.day do
  64. # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
  65. # end
  66. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  67. 23 def travel(duration, &block)
  68. travel_to Time.now + duration, &block
  69. end
  70. # Changes current time to the given time by stubbing +Time.now+,
  71. # +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
  72. # The stubs are automatically removed at the end of the test.
  73. #
  74. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  75. # travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
  76. # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
  77. # Date.current # => Wed, 24 Nov 2004
  78. # DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
  79. #
  80. # Dates are taken as their timestamp at the beginning of the day in the
  81. # application time zone. <tt>Time.current</tt> returns said timestamp,
  82. # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
  83. # <tt>Date.current</tt> returns a date equal to the argument, and
  84. # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
  85. # be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
  86. # or <tt>Date.today</tt>, in order to honor the application time zone
  87. # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
  88. #
  89. # Note that the usec for the time passed will be set to 0 to prevent rounding
  90. # errors with external services, like MySQL (which will round instead of floor,
  91. # leading to off-by-one-second errors).
  92. #
  93. # This method also accepts a block, which will return the current time back to its original
  94. # state at the end of the block:
  95. #
  96. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  97. # travel_to Time.zone.local(2004, 11, 24, 1, 4, 44) do
  98. # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
  99. # end
  100. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  101. 23 def travel_to(date_or_time)
  102. if block_given? && simple_stubs.stubbing(Time, :now)
  103. travel_to_nested_block_call = <<~MSG
  104. Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
  105. Instead of:
  106. travel_to 2.days.from_now do
  107. # 2 days from today
  108. travel_to 3.days.from_now do
  109. # 5 days from today
  110. end
  111. end
  112. preferred way to achieve above is:
  113. travel 2.days do
  114. # 2 days from today
  115. end
  116. travel 5.days do
  117. # 5 days from today
  118. end
  119. MSG
  120. raise travel_to_nested_block_call
  121. end
  122. if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
  123. now = date_or_time.midnight.to_time
  124. else
  125. now = date_or_time.to_time.change(usec: 0)
  126. end
  127. simple_stubs.stub_object(Time, :now) { at(now.to_i) }
  128. simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
  129. simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
  130. if block_given?
  131. begin
  132. yield
  133. ensure
  134. travel_back
  135. end
  136. end
  137. end
  138. # Returns the current time back to its original state, by removing the stubs added by
  139. # +travel+, +travel_to+, and +freeze_time+.
  140. #
  141. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  142. #
  143. # travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
  144. # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
  145. #
  146. # travel_back
  147. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  148. #
  149. # This method also accepts a block, which brings the stubs back at the end of the block:
  150. #
  151. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  152. #
  153. # travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
  154. # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
  155. #
  156. # travel_back do
  157. # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  158. # end
  159. #
  160. # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
  161. 23 def travel_back
  162. stubbed_time = Time.current if block_given? && simple_stubs.stubbed?
  163. simple_stubs.unstub_all!
  164. yield if block_given?
  165. ensure
  166. travel_to stubbed_time if stubbed_time
  167. end
  168. 23 alias_method :unfreeze_time, :travel_back
  169. # Calls +travel_to+ with +Time.now+.
  170. #
  171. # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
  172. # freeze_time
  173. # sleep(1)
  174. # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
  175. #
  176. # This method also accepts a block, which will return the current time back to its original
  177. # state at the end of the block:
  178. #
  179. # Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
  180. # freeze_time do
  181. # sleep(1)
  182. # User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
  183. # end
  184. # Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
  185. 23 def freeze_time(&block)
  186. travel_to Time.now, &block
  187. end
  188. 23 private
  189. 23 def simple_stubs
  190. @simple_stubs ||= SimpleStubs.new
  191. end
  192. end
  193. end
  194. end

lib/active_support/time.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module ActiveSupport
  3. 2 autoload :Duration, "active_support/duration"
  4. 2 autoload :TimeWithZone, "active_support/time_with_zone"
  5. 2 autoload :TimeZone, "active_support/values/time_zone"
  6. end
  7. 2 require "date"
  8. 2 require "time"
  9. 2 require "active_support/core_ext/time"
  10. 2 require "active_support/core_ext/date"
  11. 2 require "active_support/core_ext/date_time"
  12. 2 require "active_support/core_ext/integer/time"
  13. 2 require "active_support/core_ext/numeric/time"
  14. 2 require "active_support/core_ext/string/conversions"
  15. 2 require "active_support/core_ext/string/zones"

lib/active_support/time_with_zone.rb

46.39% lines covered

194 relevant lines. 90 lines covered and 104 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/duration"
  3. 23 require "active_support/values/time_zone"
  4. 23 require "active_support/core_ext/object/acts_like"
  5. 23 require "active_support/core_ext/date_and_time/compatibility"
  6. 23 module ActiveSupport
  7. # A Time-like class that can represent a time in any time zone. Necessary
  8. # because standard Ruby Time instances are limited to UTC and the
  9. # system's <tt>ENV['TZ']</tt> zone.
  10. #
  11. # You shouldn't ever need to create a TimeWithZone instance directly via +new+.
  12. # Instead use methods +local+, +parse+, +at+ and +now+ on TimeZone instances,
  13. # and +in_time_zone+ on Time and DateTime instances.
  14. #
  15. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  16. # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
  17. # Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
  18. # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
  19. # Time.zone.now # => Sun, 18 May 2008 13:07:55.754107581 EDT -04:00
  20. # Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
  21. #
  22. # See Time and TimeZone for further documentation of these methods.
  23. #
  24. # TimeWithZone instances implement the same API as Ruby Time instances, so
  25. # that Time and TimeWithZone instances are interchangeable.
  26. #
  27. # t = Time.zone.now # => Sun, 18 May 2008 13:27:25.031505668 EDT -04:00
  28. # t.hour # => 13
  29. # t.dst? # => true
  30. # t.utc_offset # => -14400
  31. # t.zone # => "EDT"
  32. # t.to_s(:rfc822) # => "Sun, 18 May 2008 13:27:25 -0400"
  33. # t + 1.day # => Mon, 19 May 2008 13:27:25.031505668 EDT -04:00
  34. # t.beginning_of_year # => Tue, 01 Jan 2008 00:00:00.000000000 EST -05:00
  35. # t > Time.utc(1999) # => true
  36. # t.is_a?(Time) # => true
  37. # t.is_a?(ActiveSupport::TimeWithZone) # => true
  38. 23 class TimeWithZone
  39. # Report class name as 'Time' to thwart type checking.
  40. 23 def self.name
  41. "Time"
  42. end
  43. 23 PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" }
  44. 23 PRECISIONS[0] = "%FT%T"
  45. 23 include Comparable, DateAndTime::Compatibility
  46. 23 attr_reader :time_zone
  47. 23 def initialize(utc_time, time_zone, local_time = nil, period = nil)
  48. @utc = utc_time ? transfer_time_values_to_utc_constructor(utc_time) : nil
  49. @time_zone, @time = time_zone, local_time
  50. @period = @utc ? period : get_period_and_ensure_valid_local_time(period)
  51. end
  52. # Returns a <tt>Time</tt> instance that represents the time in +time_zone+.
  53. 23 def time
  54. @time ||= incorporate_utc_offset(@utc, utc_offset)
  55. end
  56. # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
  57. 23 def utc
  58. @utc ||= incorporate_utc_offset(@time, -utc_offset)
  59. end
  60. 23 alias_method :comparable_time, :utc
  61. 23 alias_method :getgm, :utc
  62. 23 alias_method :getutc, :utc
  63. 23 alias_method :gmtime, :utc
  64. # Returns the underlying TZInfo::TimezonePeriod.
  65. 23 def period
  66. @period ||= time_zone.period_for_utc(@utc)
  67. end
  68. # Returns the simultaneous time in <tt>Time.zone</tt>, or the specified zone.
  69. 23 def in_time_zone(new_zone = ::Time.zone)
  70. return self if time_zone == new_zone
  71. utc.in_time_zone(new_zone)
  72. end
  73. # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
  74. 23 def localtime(utc_offset = nil)
  75. utc.getlocal(utc_offset)
  76. end
  77. 23 alias_method :getlocal, :localtime
  78. # Returns true if the current time is within Daylight Savings Time for the
  79. # specified time zone.
  80. #
  81. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  82. # Time.zone.parse("2012-5-30").dst? # => true
  83. # Time.zone.parse("2012-11-30").dst? # => false
  84. 23 def dst?
  85. period.dst?
  86. end
  87. 23 alias_method :isdst, :dst?
  88. # Returns true if the current time zone is set to UTC.
  89. #
  90. # Time.zone = 'UTC' # => 'UTC'
  91. # Time.zone.now.utc? # => true
  92. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  93. # Time.zone.now.utc? # => false
  94. 23 def utc?
  95. zone == "UTC" || zone == "UCT"
  96. end
  97. 23 alias_method :gmt?, :utc?
  98. # Returns the offset from current time to UTC time in seconds.
  99. 23 def utc_offset
  100. period.observed_utc_offset
  101. end
  102. 23 alias_method :gmt_offset, :utc_offset
  103. 23 alias_method :gmtoff, :utc_offset
  104. # Returns a formatted string of the offset from UTC, or an alternative
  105. # string if the time zone is already UTC.
  106. #
  107. # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
  108. # Time.zone.now.formatted_offset(true) # => "-05:00"
  109. # Time.zone.now.formatted_offset(false) # => "-0500"
  110. # Time.zone = 'UTC' # => "UTC"
  111. # Time.zone.now.formatted_offset(true, "0") # => "0"
  112. 23 def formatted_offset(colon = true, alternate_utc_string = nil)
  113. utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon)
  114. end
  115. # Returns the time zone abbreviation.
  116. #
  117. # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
  118. # Time.zone.now.zone # => "EST"
  119. 23 def zone
  120. period.abbreviation
  121. end
  122. # Returns a string of the object's date, time, zone, and offset from UTC.
  123. #
  124. # Time.zone.now.inspect # => "Thu, 04 Dec 2014 11:00:25.624541392 EST -05:00"
  125. 23 def inspect
  126. "#{time.strftime('%a, %d %b %Y %H:%M:%S.%9N')} #{zone} #{formatted_offset}"
  127. end
  128. # Returns a string of the object's date and time in the ISO 8601 standard
  129. # format.
  130. #
  131. # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
  132. 23 def xmlschema(fraction_digits = 0)
  133. "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}"
  134. end
  135. 23 alias_method :iso8601, :xmlschema
  136. 23 alias_method :rfc3339, :xmlschema
  137. # Coerces time to a string for JSON encoding. The default format is ISO 8601.
  138. # You can get %Y/%m/%d %H:%M:%S +offset style by setting
  139. # <tt>ActiveSupport::JSON::Encoding.use_standard_json_time_format</tt>
  140. # to +false+.
  141. #
  142. # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
  143. # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
  144. # # => "2005-02-01T05:15:10.000-10:00"
  145. #
  146. # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
  147. # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
  148. # # => "2005/02/01 05:15:10 -1000"
  149. 23 def as_json(options = nil)
  150. if ActiveSupport::JSON::Encoding.use_standard_json_time_format
  151. xmlschema(ActiveSupport::JSON::Encoding.time_precision)
  152. else
  153. %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
  154. end
  155. end
  156. 23 def init_with(coder) #:nodoc:
  157. initialize(coder["utc"], coder["zone"], coder["time"])
  158. end
  159. 23 def encode_with(coder) #:nodoc:
  160. coder.tag = "!ruby/object:ActiveSupport::TimeWithZone"
  161. coder.map = { "utc" => utc, "zone" => time_zone, "time" => time }
  162. end
  163. # Returns a string of the object's date and time in the format used by
  164. # HTTP requests.
  165. #
  166. # Time.zone.now.httpdate # => "Tue, 01 Jan 2013 04:39:43 GMT"
  167. 23 def httpdate
  168. utc.httpdate
  169. end
  170. # Returns a string of the object's date and time in the RFC 2822 standard
  171. # format.
  172. #
  173. # Time.zone.now.rfc2822 # => "Tue, 01 Jan 2013 04:51:39 +0000"
  174. 23 def rfc2822
  175. to_s(:rfc822)
  176. end
  177. 23 alias_method :rfc822, :rfc2822
  178. # Returns a string of the object's date and time.
  179. # Accepts an optional <tt>format</tt>:
  180. # * <tt>:default</tt> - default value, mimics Ruby Time#to_s format.
  181. # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
  182. # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
  183. 23 def to_s(format = :default)
  184. if format == :db
  185. utc.to_s(format)
  186. elsif formatter = ::Time::DATE_FORMATS[format]
  187. formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  188. else
  189. "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format
  190. end
  191. end
  192. 23 alias_method :to_formatted_s, :to_s
  193. # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime,
  194. # so that zone information is correct.
  195. 23 def strftime(format)
  196. format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}")
  197. getlocal(utc_offset).strftime(format)
  198. end
  199. # Use the time in UTC for comparisons.
  200. 23 def <=>(other)
  201. utc <=> other
  202. end
  203. 23 alias_method :before?, :<
  204. 23 alias_method :after?, :>
  205. # Returns true if the current object's time is within the specified
  206. # +min+ and +max+ time.
  207. 23 def between?(min, max)
  208. utc.between?(min, max)
  209. end
  210. # Returns true if the current object's time is in the past.
  211. 23 def past?
  212. utc.past?
  213. end
  214. # Returns true if the current object's time falls within
  215. # the current day.
  216. 23 def today?
  217. time.today?
  218. end
  219. # Returns true if the current object's time falls within
  220. # the next day (tomorrow).
  221. 23 def tomorrow?
  222. time.tomorrow?
  223. end
  224. 23 alias :next_day? :tomorrow?
  225. # Returns true if the current object's time falls within
  226. # the previous day (yesterday).
  227. 23 def yesterday?
  228. time.yesterday?
  229. end
  230. 23 alias :prev_day? :yesterday?
  231. # Returns true if the current object's time is in the future.
  232. 23 def future?
  233. utc.future?
  234. end
  235. # Returns +true+ if +other+ is equal to current object.
  236. 23 def eql?(other)
  237. other.eql?(utc)
  238. end
  239. 23 def hash
  240. utc.hash
  241. end
  242. # Adds an interval of time to the current object's time and returns that
  243. # value as a new TimeWithZone object.
  244. #
  245. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  246. # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
  247. # now + 1000 # => Sun, 02 Nov 2014 01:43:08.725182881 EDT -04:00
  248. #
  249. # If we're adding a Duration of variable length (i.e., years, months, days),
  250. # move forward from #time, otherwise move forward from #utc, for accuracy
  251. # when moving across DST boundaries.
  252. #
  253. # For instance, a time + 24.hours will advance exactly 24 hours, while a
  254. # time + 1.day will advance 23-25 hours, depending on the day.
  255. #
  256. # now + 24.hours # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
  257. # now + 1.day # => Mon, 03 Nov 2014 01:26:28.725182881 EST -05:00
  258. 23 def +(other)
  259. if duration_of_variable_length?(other)
  260. method_missing(:+, other)
  261. else
  262. result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
  263. result.in_time_zone(time_zone)
  264. end
  265. end
  266. 23 alias_method :since, :+
  267. 23 alias_method :in, :+
  268. # Subtracts an interval of time and returns a new TimeWithZone object unless
  269. # the other value `acts_like?` time. Then it will return a Float of the difference
  270. # between the two times that represents the difference between the current
  271. # object's time and the +other+ time.
  272. #
  273. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  274. # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
  275. # now - 1000 # => Mon, 03 Nov 2014 00:09:48.725182881 EST -05:00
  276. #
  277. # If subtracting a Duration of variable length (i.e., years, months, days),
  278. # move backward from #time, otherwise move backward from #utc, for accuracy
  279. # when moving across DST boundaries.
  280. #
  281. # For instance, a time - 24.hours will go subtract exactly 24 hours, while a
  282. # time - 1.day will subtract 23-25 hours, depending on the day.
  283. #
  284. # now - 24.hours # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
  285. # now - 1.day # => Sun, 02 Nov 2014 00:26:28.725182881 EDT -04:00
  286. #
  287. # If both the TimeWithZone object and the other value act like Time, a Float
  288. # will be returned.
  289. #
  290. # Time.zone.now - 1.day.ago # => 86399.999967
  291. #
  292. 23 def -(other)
  293. if other.acts_like?(:time)
  294. to_time - other.to_time
  295. elsif duration_of_variable_length?(other)
  296. method_missing(:-, other)
  297. else
  298. result = utc.acts_like?(:date) ? utc.ago(other) : utc - other rescue utc.ago(other)
  299. result.in_time_zone(time_zone)
  300. end
  301. end
  302. # Subtracts an interval of time from the current object's time and returns
  303. # the result as a new TimeWithZone object.
  304. #
  305. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  306. # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
  307. # now.ago(1000) # => Mon, 03 Nov 2014 00:09:48.725182881 EST -05:00
  308. #
  309. # If we're subtracting a Duration of variable length (i.e., years, months,
  310. # days), move backward from #time, otherwise move backward from #utc, for
  311. # accuracy when moving across DST boundaries.
  312. #
  313. # For instance, <tt>time.ago(24.hours)</tt> will move back exactly 24 hours,
  314. # while <tt>time.ago(1.day)</tt> will move back 23-25 hours, depending on
  315. # the day.
  316. #
  317. # now.ago(24.hours) # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
  318. # now.ago(1.day) # => Sun, 02 Nov 2014 00:26:28.725182881 EDT -04:00
  319. 23 def ago(other)
  320. since(-other)
  321. end
  322. # Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have
  323. # been changed according to the +options+ parameter. The time options (<tt>:hour</tt>,
  324. # <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly,
  325. # so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the
  326. # hour and minute is passed, then sec, usec and nsec is set to 0. The +options+
  327. # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>,
  328. # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>,
  329. # <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt>
  330. # or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or
  331. # <tt>:offset</tt>, not both.
  332. #
  333. # t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15.116992711 EST -05:00
  334. # t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15.116992711 EST -05:00
  335. # t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00.116992711 EST -05:00
  336. # t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00.116992711 EST -05:00
  337. # t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15.116992711 HST -10:00
  338. # t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15.116992711 HST -10:00
  339. 23 def change(options)
  340. if options[:zone] && options[:offset]
  341. raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}"
  342. end
  343. new_time = time.change(options)
  344. if options[:zone]
  345. new_zone = ::Time.find_zone(options[:zone])
  346. elsif options[:offset]
  347. new_zone = ::Time.find_zone(new_time.utc_offset)
  348. end
  349. new_zone ||= time_zone
  350. periods = new_zone.periods_for_local(new_time)
  351. self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil)
  352. end
  353. # Uses Date to provide precise Time calculations for years, months, and days
  354. # according to the proleptic Gregorian calendar. The result is returned as a
  355. # new TimeWithZone object.
  356. #
  357. # The +options+ parameter takes a hash with any of these keys:
  358. # <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>,
  359. # <tt>:hours</tt>, <tt>:minutes</tt>, <tt>:seconds</tt>.
  360. #
  361. # If advancing by a value of variable length (i.e., years, weeks, months,
  362. # days), move forward from #time, otherwise move forward from #utc, for
  363. # accuracy when moving across DST boundaries.
  364. #
  365. # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
  366. # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28.558049687 EDT -04:00
  367. # now.advance(seconds: 1) # => Sun, 02 Nov 2014 01:26:29.558049687 EDT -04:00
  368. # now.advance(minutes: 1) # => Sun, 02 Nov 2014 01:27:28.558049687 EDT -04:00
  369. # now.advance(hours: 1) # => Sun, 02 Nov 2014 01:26:28.558049687 EST -05:00
  370. # now.advance(days: 1) # => Mon, 03 Nov 2014 01:26:28.558049687 EST -05:00
  371. # now.advance(weeks: 1) # => Sun, 09 Nov 2014 01:26:28.558049687 EST -05:00
  372. # now.advance(months: 1) # => Tue, 02 Dec 2014 01:26:28.558049687 EST -05:00
  373. # now.advance(years: 1) # => Mon, 02 Nov 2015 01:26:28.558049687 EST -05:00
  374. 23 def advance(options)
  375. # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time,
  376. # otherwise advance from #utc, for accuracy when moving across DST boundaries
  377. if options.values_at(:years, :weeks, :months, :days).any?
  378. method_missing(:advance, options)
  379. else
  380. utc.advance(options).in_time_zone(time_zone)
  381. end
  382. end
  383. 23 %w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name|
  384. 299 class_eval <<-EOV, __FILE__, __LINE__ + 1
  385. def #{method_name} # def month
  386. time.#{method_name} # time.month
  387. end # end
  388. EOV
  389. end
  390. # Returns Array of parts of Time in sequence of
  391. # [seconds, minutes, hours, day, month, year, weekday, yearday, dst?, zone].
  392. #
  393. # now = Time.zone.now # => Tue, 18 Aug 2015 02:29:27.485278555 UTC +00:00
  394. # now.to_a # => [27, 29, 2, 18, 8, 2015, 2, 230, false, "UTC"]
  395. 23 def to_a
  396. [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
  397. end
  398. # Returns the object's date and time as a floating point number of seconds
  399. # since the Epoch (January 1, 1970 00:00 UTC).
  400. #
  401. # Time.zone.now.to_f # => 1417709320.285418
  402. 23 def to_f
  403. utc.to_f
  404. end
  405. # Returns the object's date and time as an integer number of seconds
  406. # since the Epoch (January 1, 1970 00:00 UTC).
  407. #
  408. # Time.zone.now.to_i # => 1417709320
  409. 23 def to_i
  410. utc.to_i
  411. end
  412. 23 alias_method :tv_sec, :to_i
  413. # Returns the object's date and time as a rational number of seconds
  414. # since the Epoch (January 1, 1970 00:00 UTC).
  415. #
  416. # Time.zone.now.to_r # => (708854548642709/500000)
  417. 23 def to_r
  418. utc.to_r
  419. end
  420. # Returns an instance of DateTime with the timezone's UTC offset
  421. #
  422. # Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000
  423. # Time.current.in_time_zone('Hawaii').to_datetime # => Mon, 17 Aug 2015 16:32:20 -1000
  424. 23 def to_datetime
  425. @to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
  426. end
  427. # Returns an instance of +Time+, either with the same UTC offset
  428. # as +self+ or in the local system timezone depending on the setting
  429. # of +ActiveSupport.to_time_preserves_timezone+.
  430. 23 def to_time
  431. if preserve_timezone
  432. @to_time_with_instance_offset ||= getlocal(utc_offset)
  433. else
  434. @to_time_with_system_offset ||= getlocal
  435. end
  436. end
  437. # So that +self+ <tt>acts_like?(:time)</tt>.
  438. 23 def acts_like_time?
  439. true
  440. end
  441. # Say we're a Time to thwart type checking.
  442. 23 def is_a?(klass)
  443. klass == ::Time || super
  444. end
  445. 23 alias_method :kind_of?, :is_a?
  446. # An instance of ActiveSupport::TimeWithZone is never blank
  447. 23 def blank?
  448. false
  449. end
  450. 23 def freeze
  451. # preload instance variables before freezing
  452. period; utc; time; to_datetime; to_time
  453. super
  454. end
  455. 23 def marshal_dump
  456. [utc, time_zone.name, time]
  457. end
  458. 23 def marshal_load(variables)
  459. initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc)
  460. end
  461. # respond_to_missing? is not called in some cases, such as when type conversion is
  462. # performed with Kernel#String
  463. 23 def respond_to?(sym, include_priv = false)
  464. # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow
  465. return false if sym.to_sym == :to_str
  466. super
  467. end
  468. # Ensure proxy class responds to all methods that underlying time instance
  469. # responds to.
  470. 23 def respond_to_missing?(sym, include_priv)
  471. return false if sym.to_sym == :acts_like_date?
  472. time.respond_to?(sym, include_priv)
  473. end
  474. # Send the missing method to +time+ instance, and wrap result in a new
  475. # TimeWithZone with the existing +time_zone+.
  476. 23 def method_missing(sym, *args, &block)
  477. wrap_with_time_zone time.__send__(sym, *args, &block)
  478. rescue NoMethodError => e
  479. raise e, e.message.sub(time.inspect, inspect), e.backtrace
  480. end
  481. 23 private
  482. 23 SECONDS_PER_DAY = 86400
  483. 23 def incorporate_utc_offset(time, offset)
  484. if time.kind_of?(Date)
  485. time + Rational(offset, SECONDS_PER_DAY)
  486. else
  487. time + offset
  488. end
  489. end
  490. 23 def get_period_and_ensure_valid_local_time(period)
  491. # we don't want a Time.local instance enforcing its own DST rules as well,
  492. # so transfer time values to a utc constructor if necessary
  493. @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
  494. begin
  495. period || @time_zone.period_for_local(@time)
  496. rescue ::TZInfo::PeriodNotFound
  497. # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
  498. @time += 1.hour
  499. retry
  500. end
  501. end
  502. 23 def transfer_time_values_to_utc_constructor(time)
  503. # avoid creating another Time object if possible
  504. return time if time.instance_of?(::Time) && time.utc?
  505. ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec)
  506. end
  507. 23 def duration_of_variable_length?(obj)
  508. ActiveSupport::Duration === obj && obj.parts.any? { |p| [:years, :months, :weeks, :days].include?(p[0]) }
  509. end
  510. 23 def wrap_with_time_zone(time)
  511. if time.acts_like?(:time)
  512. periods = time_zone.periods_for_local(time)
  513. self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil)
  514. elsif time.is_a?(Range)
  515. wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end)
  516. else
  517. time
  518. end
  519. end
  520. end
  521. end

lib/active_support/values/time_zone.rb

50.36% lines covered

137 relevant lines. 69 lines covered and 68 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "tzinfo"
  3. 23 require "concurrent/map"
  4. 23 module ActiveSupport
  5. # The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
  6. # It allows us to do the following:
  7. #
  8. # * Limit the set of zones provided by TZInfo to a meaningful subset of 134
  9. # zones.
  10. # * Retrieve and display zones with a friendlier name
  11. # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York").
  12. # * Lazily load TZInfo::Timezone instances only when they're needed.
  13. # * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+,
  14. # +parse+, +at+ and +now+ methods.
  15. #
  16. # If you set <tt>config.time_zone</tt> in the Rails Application, you can
  17. # access this TimeZone object via <tt>Time.zone</tt>:
  18. #
  19. # # application.rb:
  20. # class Application < Rails::Application
  21. # config.time_zone = 'Eastern Time (US & Canada)'
  22. # end
  23. #
  24. # Time.zone # => #<ActiveSupport::TimeZone:0x514834...>
  25. # Time.zone.name # => "Eastern Time (US & Canada)"
  26. # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00
  27. 23 class TimeZone
  28. # Keys are Rails TimeZone names, values are TZInfo identifiers.
  29. 23 MAPPING = {
  30. "International Date Line West" => "Etc/GMT+12",
  31. "Midway Island" => "Pacific/Midway",
  32. "American Samoa" => "Pacific/Pago_Pago",
  33. "Hawaii" => "Pacific/Honolulu",
  34. "Alaska" => "America/Juneau",
  35. "Pacific Time (US & Canada)" => "America/Los_Angeles",
  36. "Tijuana" => "America/Tijuana",
  37. "Mountain Time (US & Canada)" => "America/Denver",
  38. "Arizona" => "America/Phoenix",
  39. "Chihuahua" => "America/Chihuahua",
  40. "Mazatlan" => "America/Mazatlan",
  41. "Central Time (US & Canada)" => "America/Chicago",
  42. "Saskatchewan" => "America/Regina",
  43. "Guadalajara" => "America/Mexico_City",
  44. "Mexico City" => "America/Mexico_City",
  45. "Monterrey" => "America/Monterrey",
  46. "Central America" => "America/Guatemala",
  47. "Eastern Time (US & Canada)" => "America/New_York",
  48. "Indiana (East)" => "America/Indiana/Indianapolis",
  49. "Bogota" => "America/Bogota",
  50. "Lima" => "America/Lima",
  51. "Quito" => "America/Lima",
  52. "Atlantic Time (Canada)" => "America/Halifax",
  53. "Caracas" => "America/Caracas",
  54. "La Paz" => "America/La_Paz",
  55. "Santiago" => "America/Santiago",
  56. "Newfoundland" => "America/St_Johns",
  57. "Brasilia" => "America/Sao_Paulo",
  58. "Buenos Aires" => "America/Argentina/Buenos_Aires",
  59. "Montevideo" => "America/Montevideo",
  60. "Georgetown" => "America/Guyana",
  61. "Puerto Rico" => "America/Puerto_Rico",
  62. "Greenland" => "America/Godthab",
  63. "Mid-Atlantic" => "Atlantic/South_Georgia",
  64. "Azores" => "Atlantic/Azores",
  65. "Cape Verde Is." => "Atlantic/Cape_Verde",
  66. "Dublin" => "Europe/Dublin",
  67. "Edinburgh" => "Europe/London",
  68. "Lisbon" => "Europe/Lisbon",
  69. "London" => "Europe/London",
  70. "Casablanca" => "Africa/Casablanca",
  71. "Monrovia" => "Africa/Monrovia",
  72. "UTC" => "Etc/UTC",
  73. "Belgrade" => "Europe/Belgrade",
  74. "Bratislava" => "Europe/Bratislava",
  75. "Budapest" => "Europe/Budapest",
  76. "Ljubljana" => "Europe/Ljubljana",
  77. "Prague" => "Europe/Prague",
  78. "Sarajevo" => "Europe/Sarajevo",
  79. "Skopje" => "Europe/Skopje",
  80. "Warsaw" => "Europe/Warsaw",
  81. "Zagreb" => "Europe/Zagreb",
  82. "Brussels" => "Europe/Brussels",
  83. "Copenhagen" => "Europe/Copenhagen",
  84. "Madrid" => "Europe/Madrid",
  85. "Paris" => "Europe/Paris",
  86. "Amsterdam" => "Europe/Amsterdam",
  87. "Berlin" => "Europe/Berlin",
  88. "Bern" => "Europe/Zurich",
  89. "Zurich" => "Europe/Zurich",
  90. "Rome" => "Europe/Rome",
  91. "Stockholm" => "Europe/Stockholm",
  92. "Vienna" => "Europe/Vienna",
  93. "West Central Africa" => "Africa/Algiers",
  94. "Bucharest" => "Europe/Bucharest",
  95. "Cairo" => "Africa/Cairo",
  96. "Helsinki" => "Europe/Helsinki",
  97. "Kyiv" => "Europe/Kiev",
  98. "Riga" => "Europe/Riga",
  99. "Sofia" => "Europe/Sofia",
  100. "Tallinn" => "Europe/Tallinn",
  101. "Vilnius" => "Europe/Vilnius",
  102. "Athens" => "Europe/Athens",
  103. "Istanbul" => "Europe/Istanbul",
  104. "Minsk" => "Europe/Minsk",
  105. "Jerusalem" => "Asia/Jerusalem",
  106. "Harare" => "Africa/Harare",
  107. "Pretoria" => "Africa/Johannesburg",
  108. "Kaliningrad" => "Europe/Kaliningrad",
  109. "Moscow" => "Europe/Moscow",
  110. "St. Petersburg" => "Europe/Moscow",
  111. "Volgograd" => "Europe/Volgograd",
  112. "Samara" => "Europe/Samara",
  113. "Kuwait" => "Asia/Kuwait",
  114. "Riyadh" => "Asia/Riyadh",
  115. "Nairobi" => "Africa/Nairobi",
  116. "Baghdad" => "Asia/Baghdad",
  117. "Tehran" => "Asia/Tehran",
  118. "Abu Dhabi" => "Asia/Muscat",
  119. "Muscat" => "Asia/Muscat",
  120. "Baku" => "Asia/Baku",
  121. "Tbilisi" => "Asia/Tbilisi",
  122. "Yerevan" => "Asia/Yerevan",
  123. "Kabul" => "Asia/Kabul",
  124. "Ekaterinburg" => "Asia/Yekaterinburg",
  125. "Islamabad" => "Asia/Karachi",
  126. "Karachi" => "Asia/Karachi",
  127. "Tashkent" => "Asia/Tashkent",
  128. "Chennai" => "Asia/Kolkata",
  129. "Kolkata" => "Asia/Kolkata",
  130. "Mumbai" => "Asia/Kolkata",
  131. "New Delhi" => "Asia/Kolkata",
  132. "Kathmandu" => "Asia/Kathmandu",
  133. "Astana" => "Asia/Dhaka",
  134. "Dhaka" => "Asia/Dhaka",
  135. "Sri Jayawardenepura" => "Asia/Colombo",
  136. "Almaty" => "Asia/Almaty",
  137. "Novosibirsk" => "Asia/Novosibirsk",
  138. "Rangoon" => "Asia/Rangoon",
  139. "Bangkok" => "Asia/Bangkok",
  140. "Hanoi" => "Asia/Bangkok",
  141. "Jakarta" => "Asia/Jakarta",
  142. "Krasnoyarsk" => "Asia/Krasnoyarsk",
  143. "Beijing" => "Asia/Shanghai",
  144. "Chongqing" => "Asia/Chongqing",
  145. "Hong Kong" => "Asia/Hong_Kong",
  146. "Urumqi" => "Asia/Urumqi",
  147. "Kuala Lumpur" => "Asia/Kuala_Lumpur",
  148. "Singapore" => "Asia/Singapore",
  149. "Taipei" => "Asia/Taipei",
  150. "Perth" => "Australia/Perth",
  151. "Irkutsk" => "Asia/Irkutsk",
  152. "Ulaanbaatar" => "Asia/Ulaanbaatar",
  153. "Seoul" => "Asia/Seoul",
  154. "Osaka" => "Asia/Tokyo",
  155. "Sapporo" => "Asia/Tokyo",
  156. "Tokyo" => "Asia/Tokyo",
  157. "Yakutsk" => "Asia/Yakutsk",
  158. "Darwin" => "Australia/Darwin",
  159. "Adelaide" => "Australia/Adelaide",
  160. "Canberra" => "Australia/Melbourne",
  161. "Melbourne" => "Australia/Melbourne",
  162. "Sydney" => "Australia/Sydney",
  163. "Brisbane" => "Australia/Brisbane",
  164. "Hobart" => "Australia/Hobart",
  165. "Vladivostok" => "Asia/Vladivostok",
  166. "Guam" => "Pacific/Guam",
  167. "Port Moresby" => "Pacific/Port_Moresby",
  168. "Magadan" => "Asia/Magadan",
  169. "Srednekolymsk" => "Asia/Srednekolymsk",
  170. "Solomon Is." => "Pacific/Guadalcanal",
  171. "New Caledonia" => "Pacific/Noumea",
  172. "Fiji" => "Pacific/Fiji",
  173. "Kamchatka" => "Asia/Kamchatka",
  174. "Marshall Is." => "Pacific/Majuro",
  175. "Auckland" => "Pacific/Auckland",
  176. "Wellington" => "Pacific/Auckland",
  177. "Nuku'alofa" => "Pacific/Tongatapu",
  178. "Tokelau Is." => "Pacific/Fakaofo",
  179. "Chatham Is." => "Pacific/Chatham",
  180. "Samoa" => "Pacific/Apia"
  181. }
  182. 23 UTC_OFFSET_WITH_COLON = "%s%02d:%02d" # :nodoc:
  183. 23 UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") # :nodoc:
  184. 23 private_constant :UTC_OFFSET_WITH_COLON, :UTC_OFFSET_WITHOUT_COLON
  185. 23 @lazy_zones_map = Concurrent::Map.new
  186. 23 @country_zones = Concurrent::Map.new
  187. 23 class << self
  188. # Assumes self represents an offset from UTC in seconds (as returned from
  189. # Time#utc_offset) and turns this into an +HH:MM formatted string.
  190. #
  191. # ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
  192. 23 def seconds_to_utc_offset(seconds, colon = true)
  193. format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
  194. sign = (seconds < 0 ? "-" : "+")
  195. hours = seconds.abs / 3600
  196. minutes = (seconds.abs % 3600) / 60
  197. format % [sign, hours, minutes]
  198. end
  199. 23 def find_tzinfo(name)
  200. 151 TZInfo::Timezone.get(MAPPING[name] || name)
  201. end
  202. 23 alias_method :create, :new
  203. # Returns a TimeZone instance with the given name, or +nil+ if no
  204. # such TimeZone instance exists. (This exists to support the use of
  205. # this class with the +composed_of+ macro.)
  206. 23 def new(name)
  207. self[name]
  208. end
  209. # Returns an array of all TimeZone objects. There are multiple
  210. # TimeZone objects per time zone, in many cases, to make it easier
  211. # for users to find their own time zone.
  212. 23 def all
  213. 1 @zones ||= zones_map.values.sort
  214. end
  215. # Locate a specific time zone object. If the argument is a string, it
  216. # is interpreted to mean the name of the timezone to locate. If it is a
  217. # numeric value it is either the hour offset, or the second offset, of the
  218. # timezone to find. (The first one with that offset will be returned.)
  219. # Returns +nil+ if no such time zone is known to the system.
  220. 23 def [](arg)
  221. 151 case arg
  222. when String
  223. 151 begin
  224. 151 @lazy_zones_map[arg] ||= create(arg)
  225. rescue TZInfo::InvalidTimezoneIdentifier
  226. nil
  227. end
  228. when Numeric, ActiveSupport::Duration
  229. arg *= 3600 if arg.abs <= 13
  230. all.find { |z| z.utc_offset == arg.to_i }
  231. else
  232. raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
  233. end
  234. end
  235. # A convenience method for returning a collection of TimeZone objects
  236. # for time zones in the USA.
  237. 23 def us_zones
  238. country_zones(:us)
  239. end
  240. # A convenience method for returning a collection of TimeZone objects
  241. # for time zones in the country specified by its ISO 3166-1 Alpha2 code.
  242. 23 def country_zones(country_code)
  243. code = country_code.to_s.upcase
  244. @country_zones[code] ||= load_country_zones(code)
  245. end
  246. 23 def clear #:nodoc:
  247. @lazy_zones_map = Concurrent::Map.new
  248. @country_zones = Concurrent::Map.new
  249. @zones = nil
  250. @zones_map = nil
  251. end
  252. 23 private
  253. 23 def load_country_zones(code)
  254. country = TZInfo::Country.get(code)
  255. country.zone_identifiers.flat_map do |tz_id|
  256. if MAPPING.value?(tz_id)
  257. MAPPING.inject([]) do |memo, (key, value)|
  258. memo << self[key] if value == tz_id
  259. memo
  260. end
  261. else
  262. create(tz_id, nil, TZInfo::Timezone.get(tz_id))
  263. end
  264. end.sort!
  265. end
  266. 23 def zones_map
  267. 1 @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones|
  268. 151 timezone = self[name]
  269. 151 zones[name] = timezone if timezone
  270. end
  271. end
  272. end
  273. 23 include Comparable
  274. 23 attr_reader :name
  275. 23 attr_reader :tzinfo
  276. # Create a new TimeZone object with the given name and offset. The
  277. # offset is the number of seconds that this time zone is offset from UTC
  278. # (GMT). Seconds were chosen as the offset unit because that is the unit
  279. # that Ruby uses to represent time zone offsets (see Time#utc_offset).
  280. 23 def initialize(name, utc_offset = nil, tzinfo = nil)
  281. 151 @name = name
  282. 151 @utc_offset = utc_offset
  283. 151 @tzinfo = tzinfo || TimeZone.find_tzinfo(name)
  284. end
  285. # Returns the offset of this time zone from UTC in seconds.
  286. 23 def utc_offset
  287. 1996 @utc_offset || tzinfo&.current_period&.base_utc_offset
  288. end
  289. # Returns a formatted string of the offset from UTC, or an alternative
  290. # string if the time zone is already UTC.
  291. #
  292. # zone = ActiveSupport::TimeZone['Central Time (US & Canada)']
  293. # zone.formatted_offset # => "-06:00"
  294. # zone.formatted_offset(false) # => "-0600"
  295. 23 def formatted_offset(colon = true, alternate_utc_string = nil)
  296. utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon)
  297. end
  298. # Compare this time zone to the parameter. The two are compared first on
  299. # their offsets, and then by name.
  300. 23 def <=>(zone)
  301. 998 return unless zone.respond_to? :utc_offset
  302. 998 result = (utc_offset <=> zone.utc_offset)
  303. 998 result = (name <=> zone.name) if result == 0
  304. 998 result
  305. end
  306. # Compare #name and TZInfo identifier to a supplied regexp, returning +true+
  307. # if a match is found.
  308. 23 def =~(re)
  309. re === name || re === MAPPING[name]
  310. end
  311. # Compare #name and TZInfo identifier to a supplied regexp, returning +true+
  312. # if a match is found.
  313. 23 def match?(re)
  314. (re == name) || (re == MAPPING[name]) ||
  315. ((Regexp === re) && (re.match?(name) || re.match?(MAPPING[name])))
  316. end
  317. # Returns a textual representation of this time zone.
  318. 23 def to_s
  319. "(GMT#{formatted_offset}) #{name}"
  320. end
  321. # Method for creating new ActiveSupport::TimeWithZone instance in time zone
  322. # of +self+ from given values.
  323. #
  324. # Time.zone = 'Hawaii' # => "Hawaii"
  325. # Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
  326. 23 def local(*args)
  327. time = Time.utc(*args)
  328. ActiveSupport::TimeWithZone.new(nil, self, time)
  329. end
  330. # Method for creating new ActiveSupport::TimeWithZone instance in time zone
  331. # of +self+ from number of seconds since the Unix epoch.
  332. #
  333. # Time.zone = 'Hawaii' # => "Hawaii"
  334. # Time.utc(2000).to_f # => 946684800.0
  335. # Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  336. #
  337. # A second argument can be supplied to specify sub-second precision.
  338. #
  339. # Time.zone = 'Hawaii' # => "Hawaii"
  340. # Time.at(946684800, 123456.789).nsec # => 123456789
  341. 23 def at(*args)
  342. Time.at(*args).utc.in_time_zone(self)
  343. end
  344. # Method for creating new ActiveSupport::TimeWithZone instance in time zone
  345. # of +self+ from an ISO 8601 string.
  346. #
  347. # Time.zone = 'Hawaii' # => "Hawaii"
  348. # Time.zone.iso8601('1999-12-31T14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  349. #
  350. # If the time components are missing then they will be set to zero.
  351. #
  352. # Time.zone = 'Hawaii' # => "Hawaii"
  353. # Time.zone.iso8601('1999-12-31') # => Fri, 31 Dec 1999 00:00:00 HST -10:00
  354. #
  355. # If the string is invalid then an +ArgumentError+ will be raised unlike +parse+
  356. # which usually returns +nil+ when given an invalid date string.
  357. 23 def iso8601(str)
  358. parts = Date._iso8601(str)
  359. raise ArgumentError, "invalid date" if parts.empty?
  360. time = Time.new(
  361. parts.fetch(:year),
  362. parts.fetch(:mon),
  363. parts.fetch(:mday),
  364. parts.fetch(:hour, 0),
  365. parts.fetch(:min, 0),
  366. parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
  367. parts.fetch(:offset, 0)
  368. )
  369. if parts[:offset]
  370. TimeWithZone.new(time.utc, self)
  371. else
  372. TimeWithZone.new(nil, self, time)
  373. end
  374. end
  375. # Method for creating new ActiveSupport::TimeWithZone instance in time zone
  376. # of +self+ from parsed string.
  377. #
  378. # Time.zone = 'Hawaii' # => "Hawaii"
  379. # Time.zone.parse('1999-12-31 14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  380. #
  381. # If upper components are missing from the string, they are supplied from
  382. # TimeZone#now:
  383. #
  384. # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  385. # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
  386. #
  387. # However, if the date component is not provided, but any other upper
  388. # components are supplied, then the day of the month defaults to 1:
  389. #
  390. # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
  391. #
  392. # If the string is invalid then an +ArgumentError+ could be raised.
  393. 23 def parse(str, now = now())
  394. parts_to_time(Date._parse(str, false), now)
  395. end
  396. # Method for creating new ActiveSupport::TimeWithZone instance in time zone
  397. # of +self+ from an RFC 3339 string.
  398. #
  399. # Time.zone = 'Hawaii' # => "Hawaii"
  400. # Time.zone.rfc3339('2000-01-01T00:00:00Z') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  401. #
  402. # If the time or zone components are missing then an +ArgumentError+ will
  403. # be raised. This is much stricter than either +parse+ or +iso8601+ which
  404. # allow for missing components.
  405. #
  406. # Time.zone = 'Hawaii' # => "Hawaii"
  407. # Time.zone.rfc3339('1999-12-31') # => ArgumentError: invalid date
  408. 23 def rfc3339(str)
  409. parts = Date._rfc3339(str)
  410. raise ArgumentError, "invalid date" if parts.empty?
  411. time = Time.new(
  412. parts.fetch(:year),
  413. parts.fetch(:mon),
  414. parts.fetch(:mday),
  415. parts.fetch(:hour),
  416. parts.fetch(:min),
  417. parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
  418. parts.fetch(:offset)
  419. )
  420. TimeWithZone.new(time.utc, self)
  421. end
  422. # Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone.
  423. #
  424. # Assumes that +str+ is a time in the time zone +self+,
  425. # unless +format+ includes an explicit time zone.
  426. # (This is the same behavior as +parse+.)
  427. # In either case, the returned TimeWithZone has the timezone of +self+.
  428. #
  429. # Time.zone = 'Hawaii' # => "Hawaii"
  430. # Time.zone.strptime('1999-12-31 14:00:00', '%Y-%m-%d %H:%M:%S') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  431. #
  432. # If upper components are missing from the string, they are supplied from
  433. # TimeZone#now:
  434. #
  435. # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
  436. # Time.zone.strptime('22:30:00', '%H:%M:%S') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
  437. #
  438. # However, if the date component is not provided, but any other upper
  439. # components are supplied, then the day of the month defaults to 1:
  440. #
  441. # Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
  442. 23 def strptime(str, format, now = now())
  443. parts_to_time(DateTime._strptime(str, format), now)
  444. end
  445. # Returns an ActiveSupport::TimeWithZone instance representing the current
  446. # time in the time zone represented by +self+.
  447. #
  448. # Time.zone = 'Hawaii' # => "Hawaii"
  449. # Time.zone.now # => Wed, 23 Jan 2008 20:24:27 HST -10:00
  450. 23 def now
  451. time_now.utc.in_time_zone(self)
  452. end
  453. # Returns the current date in this time zone.
  454. 23 def today
  455. tzinfo.now.to_date
  456. end
  457. # Returns the next date in this time zone.
  458. 23 def tomorrow
  459. today + 1
  460. end
  461. # Returns the previous date in this time zone.
  462. 23 def yesterday
  463. today - 1
  464. end
  465. # Adjust the given time to the simultaneous time in the time zone
  466. # represented by +self+. Returns a local time with the appropriate offset
  467. # -- if you want an ActiveSupport::TimeWithZone instance, use
  468. # Time#in_time_zone() instead.
  469. #
  470. # As of tzinfo 2, utc_to_local returns a Time with a non-zero utc_offset.
  471. # See the `utc_to_local_returns_utc_offset_times` config for more info.
  472. 23 def utc_to_local(time)
  473. tzinfo.utc_to_local(time).yield_self do |t|
  474. ActiveSupport.utc_to_local_returns_utc_offset_times ?
  475. t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction)
  476. end
  477. end
  478. # Adjust the given time to the simultaneous time in UTC. Returns a
  479. # Time.utc() instance.
  480. 23 def local_to_utc(time, dst = true)
  481. tzinfo.local_to_utc(time, dst)
  482. end
  483. # Available so that TimeZone instances respond like TZInfo::Timezone
  484. # instances.
  485. 23 def period_for_utc(time)
  486. tzinfo.period_for_utc(time)
  487. end
  488. # Available so that TimeZone instances respond like TZInfo::Timezone
  489. # instances.
  490. 23 def period_for_local(time, dst = true)
  491. tzinfo.period_for_local(time, dst) { |periods| periods.last }
  492. end
  493. 23 def periods_for_local(time) #:nodoc:
  494. tzinfo.periods_for_local(time)
  495. end
  496. 23 def init_with(coder) #:nodoc:
  497. initialize(coder["name"])
  498. end
  499. 23 def encode_with(coder) #:nodoc:
  500. coder.tag = "!ruby/object:#{self.class}"
  501. coder.map = { "name" => tzinfo.name }
  502. end
  503. 23 private
  504. 23 def parts_to_time(parts, now)
  505. raise ArgumentError, "invalid date" if parts.nil?
  506. return if parts.empty?
  507. if parts[:seconds]
  508. time = Time.at(parts[:seconds])
  509. else
  510. time = Time.new(
  511. parts.fetch(:year, now.year),
  512. parts.fetch(:mon, now.month),
  513. parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
  514. parts.fetch(:hour, 0),
  515. parts.fetch(:min, 0),
  516. parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
  517. parts.fetch(:offset, 0)
  518. )
  519. end
  520. if parts[:offset] || parts[:seconds]
  521. TimeWithZone.new(time.utc, self)
  522. else
  523. TimeWithZone.new(nil, self, time)
  524. end
  525. end
  526. 23 def time_now
  527. Time.now
  528. end
  529. end
  530. end

lib/active_support/version.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 24 require_relative "gem_version"
  3. 24 module ActiveSupport
  4. # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
  5. 24 def self.version
  6. gem_version
  7. end
  8. end

lib/active_support/xml_mini.rb

41.12% lines covered

107 relevant lines. 44 lines covered and 63 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "time"
  3. 23 require "base64"
  4. 23 require "bigdecimal"
  5. 23 require "bigdecimal/util"
  6. 23 require "active_support/core_ext/module/delegation"
  7. 23 require "active_support/core_ext/string/inflections"
  8. 23 require "active_support/core_ext/date_time/calculations"
  9. 23 module ActiveSupport
  10. # = XmlMini
  11. #
  12. # To use the much faster libxml parser:
  13. # gem 'libxml-ruby', '=0.9.7'
  14. # XmlMini.backend = 'LibXML'
  15. 23 module XmlMini
  16. 23 extend self
  17. # This module decorates files deserialized using Hash.from_xml with
  18. # the <tt>original_filename</tt> and <tt>content_type</tt> methods.
  19. 23 module FileLike #:nodoc:
  20. 23 attr_writer :original_filename, :content_type
  21. 23 def original_filename
  22. @original_filename || "untitled"
  23. end
  24. 23 def content_type
  25. @content_type || "application/octet-stream"
  26. end
  27. end
  28. DEFAULT_ENCODINGS = {
  29. "binary" => "base64"
  30. 23 } unless defined?(DEFAULT_ENCODINGS)
  31. 23 unless defined?(TYPE_NAMES)
  32. 23 TYPE_NAMES = {
  33. "Symbol" => "symbol",
  34. "Integer" => "integer",
  35. "BigDecimal" => "decimal",
  36. "Float" => "float",
  37. "TrueClass" => "boolean",
  38. "FalseClass" => "boolean",
  39. "Date" => "date",
  40. "DateTime" => "dateTime",
  41. "Time" => "dateTime",
  42. "Array" => "array",
  43. "Hash" => "hash"
  44. }
  45. end
  46. FORMATTING = {
  47. "symbol" => Proc.new { |symbol| symbol.to_s },
  48. "date" => Proc.new { |date| date.to_s(:db) },
  49. "dateTime" => Proc.new { |time| time.xmlschema },
  50. "binary" => Proc.new { |binary| ::Base64.encode64(binary) },
  51. "yaml" => Proc.new { |yaml| yaml.to_yaml }
  52. 23 } unless defined?(FORMATTING)
  53. # TODO use regexp instead of Date.parse
  54. 23 unless defined?(PARSING)
  55. 23 PARSING = {
  56. "symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
  57. "date" => Proc.new { |date| ::Date.parse(date) },
  58. "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
  59. "integer" => Proc.new { |integer| integer.to_i },
  60. "float" => Proc.new { |float| float.to_f },
  61. "decimal" => Proc.new do |number|
  62. if String === number
  63. number.to_d
  64. else
  65. BigDecimal(number)
  66. end
  67. end,
  68. "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
  69. "string" => Proc.new { |string| string.to_s },
  70. "yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml },
  71. "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
  72. "binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
  73. "file" => Proc.new { |file, entity| _parse_file(file, entity) }
  74. }
  75. 23 PARSING.update(
  76. "double" => PARSING["float"],
  77. "dateTime" => PARSING["datetime"]
  78. )
  79. end
  80. 23 attr_accessor :depth
  81. 23 self.depth = 100
  82. 23 delegate :parse, to: :backend
  83. 23 def backend
  84. current_thread_backend || @backend
  85. end
  86. 23 def backend=(name)
  87. 23 backend = name && cast_backend_name_to_module(name)
  88. 23 self.current_thread_backend = backend if current_thread_backend
  89. 23 @backend = backend
  90. end
  91. 23 def with_backend(name)
  92. old_backend = current_thread_backend
  93. self.current_thread_backend = name && cast_backend_name_to_module(name)
  94. yield
  95. ensure
  96. self.current_thread_backend = old_backend
  97. end
  98. 23 def to_tag(key, value, options)
  99. type_name = options.delete(:type)
  100. merged_options = options.merge(root: key, skip_instruct: true)
  101. if value.is_a?(::Method) || value.is_a?(::Proc)
  102. if value.arity == 1
  103. value.call(merged_options)
  104. else
  105. value.call(merged_options, key.to_s.singularize)
  106. end
  107. elsif value.respond_to?(:to_xml)
  108. value.to_xml(merged_options)
  109. else
  110. type_name ||= TYPE_NAMES[value.class.name]
  111. type_name ||= value.class.name if value && !value.respond_to?(:to_str)
  112. type_name = type_name.to_s if type_name
  113. type_name = "dateTime" if type_name == "datetime"
  114. key = rename_key(key.to_s, options)
  115. attributes = options[:skip_types] || type_name.nil? ? {} : { type: type_name }
  116. attributes[:nil] = true if value.nil?
  117. encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name]
  118. attributes[:encoding] = encoding if encoding
  119. formatted_value = FORMATTING[type_name] && !value.nil? ?
  120. FORMATTING[type_name].call(value) : value
  121. options[:builder].tag!(key, formatted_value, attributes)
  122. end
  123. end
  124. 23 def rename_key(key, options = {})
  125. camelize = options[:camelize]
  126. dasherize = !options.has_key?(:dasherize) || options[:dasherize]
  127. if camelize
  128. key = true == camelize ? key.camelize : key.camelize(camelize)
  129. end
  130. key = _dasherize(key) if dasherize
  131. key
  132. end
  133. 23 private
  134. 23 def _dasherize(key)
  135. # $2 must be a non-greedy regex for this to work
  136. left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1, 3]
  137. "#{left}#{middle.tr('_ ', '--')}#{right}"
  138. end
  139. # TODO: Add support for other encodings
  140. 23 def _parse_binary(bin, entity)
  141. case entity["encoding"]
  142. when "base64"
  143. ::Base64.decode64(bin)
  144. else
  145. bin
  146. end
  147. end
  148. 23 def _parse_file(file, entity)
  149. f = StringIO.new(::Base64.decode64(file))
  150. f.extend(FileLike)
  151. f.original_filename = entity["name"]
  152. f.content_type = entity["content_type"]
  153. f
  154. end
  155. 23 def current_thread_backend
  156. 23 Thread.current[:xml_mini_backend]
  157. end
  158. 23 def current_thread_backend=(name)
  159. Thread.current[:xml_mini_backend] = name && cast_backend_name_to_module(name)
  160. end
  161. 23 def cast_backend_name_to_module(name)
  162. 23 if name.is_a?(Module)
  163. name
  164. else
  165. 23 require "active_support/xml_mini/#{name.downcase}"
  166. 23 ActiveSupport.const_get("XmlMini_#{name}")
  167. end
  168. end
  169. end
  170. 23 XmlMini.backend = "REXML"
  171. end

lib/active_support/xml_mini/jdom.rb

0.0% lines covered

116 relevant lines. 0 lines covered and 116 lines missed.
    
  1. # frozen_string_literal: true
  2. raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFORM.include?("java")
  3. require "jruby"
  4. include Java
  5. require "active_support/core_ext/object/blank"
  6. java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder
  7. java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory
  8. java_import java.io.StringReader unless defined? StringReader
  9. java_import org.xml.sax.InputSource unless defined? InputSource
  10. java_import org.xml.sax.Attributes unless defined? Attributes
  11. java_import org.w3c.dom.Node unless defined? Node
  12. module ActiveSupport
  13. module XmlMini_JDOM #:nodoc:
  14. extend self
  15. CONTENT_KEY = "__content__"
  16. NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE
  17. DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE
  18. PROCESSING_INSTRUCTION_NODE TEXT_NODE}
  19. node_type_map = {}
  20. NODE_TYPE_NAMES.each { |type| node_type_map[Node.send(type)] = type }
  21. # Parse an XML Document string or IO into a simple hash using Java's jdom.
  22. # data::
  23. # XML Document string or IO to parse
  24. def parse(data)
  25. if data.respond_to?(:read)
  26. data = data.read
  27. end
  28. if data.blank?
  29. {}
  30. else
  31. @dbf = DocumentBuilderFactory.new_instance
  32. # secure processing of java xml
  33. # https://archive.is/9xcQQ
  34. @dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
  35. @dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
  36. @dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
  37. @dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true)
  38. xml_string_reader = StringReader.new(data)
  39. xml_input_source = InputSource.new(xml_string_reader)
  40. doc = @dbf.new_document_builder.parse(xml_input_source)
  41. merge_element!({ CONTENT_KEY => "" }, doc.document_element, XmlMini.depth)
  42. end
  43. end
  44. private
  45. # Convert an XML element and merge into the hash
  46. #
  47. # hash::
  48. # Hash to merge the converted element into.
  49. # element::
  50. # XML element to merge into hash
  51. def merge_element!(hash, element, depth)
  52. raise "Document too deep!" if depth == 0
  53. delete_empty(hash)
  54. merge!(hash, element.tag_name, collapse(element, depth))
  55. end
  56. def delete_empty(hash)
  57. hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ""
  58. end
  59. # Actually converts an XML document element into a data structure.
  60. #
  61. # element::
  62. # The document element to be collapsed.
  63. def collapse(element, depth)
  64. hash = get_attributes(element)
  65. child_nodes = element.child_nodes
  66. if child_nodes.length > 0
  67. (0...child_nodes.length).each do |i|
  68. child = child_nodes.item(i)
  69. merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
  70. end
  71. merge_texts!(hash, element) unless empty_content?(element)
  72. hash
  73. else
  74. merge_texts!(hash, element)
  75. end
  76. end
  77. # Merge all the texts of an element into the hash
  78. #
  79. # hash::
  80. # Hash to add the converted element to.
  81. # element::
  82. # XML element whose texts are to me merged into the hash
  83. def merge_texts!(hash, element)
  84. delete_empty(hash)
  85. text_children = texts(element)
  86. if text_children.join.empty?
  87. hash
  88. else
  89. # must use value to prevent double-escaping
  90. merge!(hash, CONTENT_KEY, text_children.join)
  91. end
  92. end
  93. # Adds a new key/value pair to an existing Hash. If the key to be added
  94. # already exists and the existing value associated with key is not
  95. # an Array, it will be wrapped in an Array. Then the new value is
  96. # appended to that Array.
  97. #
  98. # hash::
  99. # Hash to add key/value pair to.
  100. # key::
  101. # Key to be added.
  102. # value::
  103. # Value to be associated with key.
  104. def merge!(hash, key, value)
  105. if hash.has_key?(key)
  106. if hash[key].instance_of?(Array)
  107. hash[key] << value
  108. else
  109. hash[key] = [hash[key], value]
  110. end
  111. elsif value.instance_of?(Array)
  112. hash[key] = [value]
  113. else
  114. hash[key] = value
  115. end
  116. hash
  117. end
  118. # Converts the attributes array of an XML element into a hash.
  119. # Returns an empty Hash if node has no attributes.
  120. #
  121. # element::
  122. # XML element to extract attributes from.
  123. def get_attributes(element)
  124. attribute_hash = {}
  125. attributes = element.attributes
  126. (0...attributes.length).each do |i|
  127. attribute_hash[CONTENT_KEY] ||= ""
  128. attribute_hash[attributes.item(i).name] = attributes.item(i).value
  129. end
  130. attribute_hash
  131. end
  132. # Determines if a document element has text content
  133. #
  134. # element::
  135. # XML element to be checked.
  136. def texts(element)
  137. texts = []
  138. child_nodes = element.child_nodes
  139. (0...child_nodes.length).each do |i|
  140. item = child_nodes.item(i)
  141. if item.node_type == Node.TEXT_NODE
  142. texts << item.get_data
  143. end
  144. end
  145. texts
  146. end
  147. # Determines if a document element has text content
  148. #
  149. # element::
  150. # XML element to be checked.
  151. def empty_content?(element)
  152. text = +""
  153. child_nodes = element.child_nodes
  154. (0...child_nodes.length).each do |i|
  155. item = child_nodes.item(i)
  156. if item.node_type == Node.TEXT_NODE
  157. text << item.get_data.strip
  158. end
  159. end
  160. text.strip.length == 0
  161. end
  162. end
  163. end

lib/active_support/xml_mini/libxml.rb

0.0% lines covered

53 relevant lines. 0 lines covered and 53 lines missed.
    
  1. # frozen_string_literal: true
  2. require "libxml"
  3. require "active_support/core_ext/object/blank"
  4. require "stringio"
  5. module ActiveSupport
  6. module XmlMini_LibXML #:nodoc:
  7. extend self
  8. # Parse an XML Document string or IO into a simple hash using libxml.
  9. # data::
  10. # XML Document string or IO to parse
  11. def parse(data)
  12. if !data.respond_to?(:read)
  13. data = StringIO.new(data || "")
  14. end
  15. if data.eof?
  16. {}
  17. else
  18. LibXML::XML::Parser.io(data).parse.to_hash
  19. end
  20. end
  21. end
  22. end
  23. module LibXML #:nodoc:
  24. module Conversions #:nodoc:
  25. module Document #:nodoc:
  26. def to_hash
  27. root.to_hash
  28. end
  29. end
  30. module Node #:nodoc:
  31. CONTENT_ROOT = "__content__"
  32. # Convert XML document to hash.
  33. #
  34. # hash::
  35. # Hash to merge the converted element into.
  36. def to_hash(hash = {})
  37. node_hash = {}
  38. # Insert node hash into parent hash correctly.
  39. case hash[name]
  40. when Array then hash[name] << node_hash
  41. when Hash then hash[name] = [hash[name], node_hash]
  42. when nil then hash[name] = node_hash
  43. end
  44. # Handle child elements
  45. each_child do |c|
  46. if c.element?
  47. c.to_hash(node_hash)
  48. elsif c.text? || c.cdata?
  49. node_hash[CONTENT_ROOT] ||= +""
  50. node_hash[CONTENT_ROOT] << c.content
  51. end
  52. end
  53. # Remove content node if it is blank
  54. if node_hash.length > 1 && node_hash[CONTENT_ROOT].blank?
  55. node_hash.delete(CONTENT_ROOT)
  56. end
  57. # Handle attributes
  58. each_attr { |a| node_hash[a.name] = a.value }
  59. hash
  60. end
  61. end
  62. end
  63. end
  64. # :enddoc:
  65. LibXML::XML::Document.include(LibXML::Conversions::Document)
  66. LibXML::XML::Node.include(LibXML::Conversions::Node)

lib/active_support/xml_mini/libxmlsax.rb

0.0% lines covered

62 relevant lines. 0 lines covered and 62 lines missed.
    
  1. # frozen_string_literal: true
  2. require "libxml"
  3. require "active_support/core_ext/object/blank"
  4. require "stringio"
  5. module ActiveSupport
  6. module XmlMini_LibXMLSAX #:nodoc:
  7. extend self
  8. # Class that will build the hash while the XML document
  9. # is being parsed using SAX events.
  10. class HashBuilder
  11. include LibXML::XML::SaxParser::Callbacks
  12. CONTENT_KEY = "__content__"
  13. HASH_SIZE_KEY = "__hash_size__"
  14. attr_reader :hash
  15. def current_hash
  16. @hash_stack.last
  17. end
  18. def on_start_document
  19. @hash = { CONTENT_KEY => +"" }
  20. @hash_stack = [@hash]
  21. end
  22. def on_end_document
  23. @hash = @hash_stack.pop
  24. @hash.delete(CONTENT_KEY)
  25. end
  26. def on_start_element(name, attrs = {})
  27. new_hash = { CONTENT_KEY => +"" }.merge!(attrs)
  28. new_hash[HASH_SIZE_KEY] = new_hash.size + 1
  29. case current_hash[name]
  30. when Array then current_hash[name] << new_hash
  31. when Hash then current_hash[name] = [current_hash[name], new_hash]
  32. when nil then current_hash[name] = new_hash
  33. end
  34. @hash_stack.push(new_hash)
  35. end
  36. def on_end_element(name)
  37. if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
  38. current_hash.delete(CONTENT_KEY)
  39. end
  40. @hash_stack.pop
  41. end
  42. def on_characters(string)
  43. current_hash[CONTENT_KEY] << string
  44. end
  45. alias_method :on_cdata_block, :on_characters
  46. end
  47. attr_accessor :document_class
  48. self.document_class = HashBuilder
  49. def parse(data)
  50. if !data.respond_to?(:read)
  51. data = StringIO.new(data || "")
  52. end
  53. if data.eof?
  54. {}
  55. else
  56. LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
  57. parser = LibXML::XML::SaxParser.io(data)
  58. document = document_class.new
  59. parser.callbacks = document
  60. parser.parse
  61. document.hash
  62. end
  63. end
  64. end
  65. end

lib/active_support/xml_mini/nokogiri.rb

0.0% lines covered

58 relevant lines. 0 lines covered and 58 lines missed.
    
  1. # frozen_string_literal: true
  2. begin
  3. require "nokogiri"
  4. rescue LoadError => e
  5. $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
  6. raise e
  7. end
  8. require "active_support/core_ext/object/blank"
  9. require "stringio"
  10. module ActiveSupport
  11. module XmlMini_Nokogiri #:nodoc:
  12. extend self
  13. # Parse an XML Document string or IO into a simple hash using libxml / nokogiri.
  14. # data::
  15. # XML Document string or IO to parse
  16. def parse(data)
  17. if !data.respond_to?(:read)
  18. data = StringIO.new(data || "")
  19. end
  20. if data.eof?
  21. {}
  22. else
  23. doc = Nokogiri::XML(data)
  24. raise doc.errors.first if doc.errors.length > 0
  25. doc.to_hash
  26. end
  27. end
  28. module Conversions #:nodoc:
  29. module Document #:nodoc:
  30. def to_hash
  31. root.to_hash
  32. end
  33. end
  34. module Node #:nodoc:
  35. CONTENT_ROOT = "__content__"
  36. # Convert XML document to hash.
  37. #
  38. # hash::
  39. # Hash to merge the converted element into.
  40. def to_hash(hash = {})
  41. node_hash = {}
  42. # Insert node hash into parent hash correctly.
  43. case hash[name]
  44. when Array then hash[name] << node_hash
  45. when Hash then hash[name] = [hash[name], node_hash]
  46. when nil then hash[name] = node_hash
  47. end
  48. # Handle child elements
  49. children.each do |c|
  50. if c.element?
  51. c.to_hash(node_hash)
  52. elsif c.text? || c.cdata?
  53. node_hash[CONTENT_ROOT] ||= +""
  54. node_hash[CONTENT_ROOT] << c.content
  55. end
  56. end
  57. # Remove content node if it is blank and there are child tags
  58. if node_hash.length > 1 && node_hash[CONTENT_ROOT].blank?
  59. node_hash.delete(CONTENT_ROOT)
  60. end
  61. # Handle attributes
  62. attribute_nodes.each { |a| node_hash[a.node_name] = a.value }
  63. hash
  64. end
  65. end
  66. end
  67. Nokogiri::XML::Document.include(Conversions::Document)
  68. Nokogiri::XML::Node.include(Conversions::Node)
  69. end
  70. end

lib/active_support/xml_mini/nokogirisax.rb

0.0% lines covered

66 relevant lines. 0 lines covered and 66 lines missed.
    
  1. # frozen_string_literal: true
  2. begin
  3. require "nokogiri"
  4. rescue LoadError => e
  5. $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
  6. raise e
  7. end
  8. require "active_support/core_ext/object/blank"
  9. require "stringio"
  10. module ActiveSupport
  11. module XmlMini_NokogiriSAX #:nodoc:
  12. extend self
  13. # Class that will build the hash while the XML document
  14. # is being parsed using SAX events.
  15. class HashBuilder < Nokogiri::XML::SAX::Document
  16. CONTENT_KEY = "__content__"
  17. HASH_SIZE_KEY = "__hash_size__"
  18. attr_reader :hash
  19. def current_hash
  20. @hash_stack.last
  21. end
  22. def start_document
  23. @hash = {}
  24. @hash_stack = [@hash]
  25. end
  26. def end_document
  27. raise "Parse stack not empty!" if @hash_stack.size > 1
  28. end
  29. def error(error_message)
  30. raise error_message
  31. end
  32. def start_element(name, attrs = [])
  33. new_hash = { CONTENT_KEY => +"" }.merge!(Hash[attrs])
  34. new_hash[HASH_SIZE_KEY] = new_hash.size + 1
  35. case current_hash[name]
  36. when Array then current_hash[name] << new_hash
  37. when Hash then current_hash[name] = [current_hash[name], new_hash]
  38. when nil then current_hash[name] = new_hash
  39. end
  40. @hash_stack.push(new_hash)
  41. end
  42. def end_element(name)
  43. if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
  44. current_hash.delete(CONTENT_KEY)
  45. end
  46. @hash_stack.pop
  47. end
  48. def characters(string)
  49. current_hash[CONTENT_KEY] << string
  50. end
  51. alias_method :cdata_block, :characters
  52. end
  53. attr_accessor :document_class
  54. self.document_class = HashBuilder
  55. def parse(data)
  56. if !data.respond_to?(:read)
  57. data = StringIO.new(data || "")
  58. end
  59. if data.eof?
  60. {}
  61. else
  62. document = document_class.new
  63. parser = Nokogiri::XML::SAX::Parser.new(document)
  64. parser.parse(data)
  65. document.hash
  66. end
  67. end
  68. end
  69. end

lib/active_support/xml_mini/rexml.rb

30.61% lines covered

49 relevant lines. 15 lines covered and 34 lines missed.
    
  1. # frozen_string_literal: true
  2. 23 require "active_support/core_ext/kernel/reporting"
  3. 23 require "active_support/core_ext/object/blank"
  4. 23 require "stringio"
  5. 23 module ActiveSupport
  6. 23 module XmlMini_REXML #:nodoc:
  7. 23 extend self
  8. 23 CONTENT_KEY = "__content__"
  9. # Parse an XML Document string or IO into a simple hash.
  10. #
  11. # Same as XmlSimple::xml_in but doesn't shoot itself in the foot,
  12. # and uses the defaults from Active Support.
  13. #
  14. # data::
  15. # XML Document string or IO to parse
  16. 23 def parse(data)
  17. if !data.respond_to?(:read)
  18. data = StringIO.new(data || "")
  19. end
  20. if data.eof?
  21. {}
  22. else
  23. silence_warnings { require "rexml/document" } unless defined?(REXML::Document)
  24. doc = REXML::Document.new(data)
  25. if doc.root
  26. merge_element!({}, doc.root, XmlMini.depth)
  27. else
  28. raise REXML::ParseException,
  29. "The document #{doc.to_s.inspect} does not have a valid root"
  30. end
  31. end
  32. end
  33. 23 private
  34. # Convert an XML element and merge into the hash
  35. #
  36. # hash::
  37. # Hash to merge the converted element into.
  38. # element::
  39. # XML element to merge into hash
  40. 23 def merge_element!(hash, element, depth)
  41. raise REXML::ParseException, "The document is too deep" if depth == 0
  42. merge!(hash, element.name, collapse(element, depth))
  43. end
  44. # Actually converts an XML document element into a data structure.
  45. #
  46. # element::
  47. # The document element to be collapsed.
  48. 23 def collapse(element, depth)
  49. hash = get_attributes(element)
  50. if element.has_elements?
  51. element.each_element { |child| merge_element!(hash, child, depth - 1) }
  52. merge_texts!(hash, element) unless empty_content?(element)
  53. hash
  54. else
  55. merge_texts!(hash, element)
  56. end
  57. end
  58. # Merge all the texts of an element into the hash
  59. #
  60. # hash::
  61. # Hash to add the converted element to.
  62. # element::
  63. # XML element whose texts are to me merged into the hash
  64. 23 def merge_texts!(hash, element)
  65. unless element.has_text?
  66. hash
  67. else
  68. # must use value to prevent double-escaping
  69. texts = +""
  70. element.texts.each { |t| texts << t.value }
  71. merge!(hash, CONTENT_KEY, texts)
  72. end
  73. end
  74. # Adds a new key/value pair to an existing Hash. If the key to be added
  75. # already exists and the existing value associated with key is not
  76. # an Array, it will be wrapped in an Array. Then the new value is
  77. # appended to that Array.
  78. #
  79. # hash::
  80. # Hash to add key/value pair to.
  81. # key::
  82. # Key to be added.
  83. # value::
  84. # Value to be associated with key.
  85. 23 def merge!(hash, key, value)
  86. if hash.has_key?(key)
  87. if hash[key].instance_of?(Array)
  88. hash[key] << value
  89. else
  90. hash[key] = [hash[key], value]
  91. end
  92. elsif value.instance_of?(Array)
  93. hash[key] = [value]
  94. else
  95. hash[key] = value
  96. end
  97. hash
  98. end
  99. # Converts the attributes array of an XML element into a hash.
  100. # Returns an empty Hash if node has no attributes.
  101. #
  102. # element::
  103. # XML element to extract attributes from.
  104. 23 def get_attributes(element)
  105. attributes = {}
  106. element.attributes.each { |n, v| attributes[n] = v }
  107. attributes
  108. end
  109. # Determines if a document element has text content
  110. #
  111. # element::
  112. # XML element to be checked.
  113. 23 def empty_content?(element)
  114. element.texts.join.blank?
  115. end
  116. end
  117. end