json-path-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dev/setup.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sets up environment for running specs and via irb e.g. `$ irb -r ./dev/setup`
4
+ require 'delegate'
5
+ require "pathname"
6
+ require "rudash"
7
+
8
+ require 'rspec/core'
9
+
10
+ require 'active_support/core_ext/hash/keys'
11
+ require 'active_support/core_ext/module/delegation'
12
+ require 'active_support/core_ext/object/blank'
13
+ require 'active_support/core_ext/object/json'
14
+ require 'active_support/core_ext/enumerable'
15
+
16
+ %w[../../lib/json_path ../../spec/spec_helper].each do |rel_path|
17
+ require File.expand_path(rel_path, Pathname.new(__FILE__).realpath)
18
+ end
19
+
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require File.expand_path('lib/json-path/version', __dir__)
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.authors = ["Desmond O'Leary"]
8
+ gem.email = ["desoleary@gmail.com"]
9
+ gem.description = 'Declarative mapping JSON/Hash data structures'
10
+ gem.summary = 'Declarative mapping JSON/Hash data structures'
11
+ gem.homepage = "https://github.com/omnitech-solutions/json-path-builder"
12
+ gem.license = "MIT"
13
+
14
+ gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
15
+ gem.executables = gem.files.grep(%r{^exe/}).map { |f| File.basename(f) }
16
+ gem.name = "json-path-builder"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = JsonPathBuilder::VERSION
19
+ gem.required_ruby_version = ">= 2.7"
20
+
21
+ gem.metadata["homepage_uri"] = gem.homepage
22
+ gem.metadata["source_code_uri"] = gem.homepage
23
+ gem.metadata["changelog_uri"] = "#{gem.homepage}/CHANGELOG.md"
24
+
25
+ gem.add_runtime_dependency 'activesupport', '>= 5'
26
+ gem.add_runtime_dependency 'rordash', '~> 0.1.2'
27
+
28
+ gem.add_development_dependency("codecov", "~> 0.6.0")
29
+ gem.add_development_dependency("rake", "~> 13.0.6")
30
+ gem.add_development_dependency("rspec", "~> 3.12.0")
31
+ gem.add_development_dependency("simplecov", "~> 0.21.2")
32
+ gem.metadata['rubygems_mfa_required'] = 'true'
33
+ end
@@ -0,0 +1,232 @@
1
+ module JsonPath
2
+ # rubocop:disable Metrics/ClassLength
3
+ class Builder
4
+ attr_reader :path_context_collection, :parent_path_context
5
+
6
+ delegate :source_data, :nested_paths, :data_wrapper_class, :reject_from_paths!, to: :path_context_collection,
7
+ allow_nil: true
8
+
9
+ def initialize(parent_path_context: nil)
10
+ @parent_path_context = parent_path_context
11
+ @path_context_collection = PathContextCollection.new(self)
12
+ end
13
+
14
+ def within(json_path, &block)
15
+ path_context_collection.within(json_path, &block)
16
+
17
+ self
18
+ end
19
+
20
+ def with_wrapped_data_class(klass)
21
+ path_context_collection.with_wrapped_data_class(klass)
22
+
23
+ self
24
+ end
25
+
26
+ # rubocop:disable Metrics/ParameterLists
27
+ def from(json_path, to: nil, transform: nil, defaults: nil, fallback: nil, transform_with_builder: false)
28
+ @path_context_collection.add_path(json_path, self,
29
+ iterable_data: false,
30
+ to: to,
31
+ transform: transform,
32
+ use_builder: transform_with_builder,
33
+ defaults: defaults,
34
+ fallback_proc: fallback,
35
+ skip_if_proc: nil)
36
+
37
+ self
38
+ end
39
+ # rubocop:enable Metrics/ParameterLists
40
+
41
+ # rubocop:disable Metrics/ParameterLists
42
+ def from_each(json_path, to: nil, transform: nil, skip_if: nil, defaults: nil, fallback: nil,
43
+ transform_with_builder: false)
44
+ @path_context_collection.add_path(json_path, self,
45
+ to: to,
46
+ iterable_data: true,
47
+ transform: transform,
48
+ use_builder: transform_with_builder,
49
+ defaults: defaults,
50
+ fallback_proc: fallback,
51
+ skip_if_proc: skip_if)
52
+
53
+ self
54
+ end
55
+ # rubocop:enable Metrics/ParameterLists
56
+
57
+ def with_source_data(data)
58
+ path_context_collection.with_source_data(data)
59
+
60
+ self
61
+ end
62
+
63
+ def without_from_paths!(from_paths_to_remove)
64
+ path_context_collection.reject_from_paths!(from_paths_to_remove)
65
+
66
+ self
67
+ end
68
+
69
+ def build
70
+ raise 'source data must be filled' if source_data.nil?
71
+
72
+ build_for(source_data)
73
+ end
74
+
75
+ def build_for(data, &each_value_block)
76
+ path_contexts = with_source_data(data).path_context_collection
77
+ self.class.build_for(path_contexts, &each_value_block)
78
+ end
79
+
80
+ def paths?
81
+ @path_context_collection.count.positive?
82
+ end
83
+
84
+ class << self
85
+ def build_for(path_contexts, &each_value_block)
86
+ mapped_data = {}
87
+ identity_proc = proc { |val| val }
88
+ each_value_block = identity_proc unless each_value_block.is_a?(Proc)
89
+
90
+ path_contexts.each do |path_context|
91
+ path_context.with_prev_mapped_data(mapped_data)
92
+
93
+ if path_context.unmatched_nested?
94
+ set_mapped_value(mapped_data, key: path_context.to, value: get_fallback_value(path_context))
95
+ next path_context
96
+ end
97
+
98
+ picked_value = value_at(path_context)
99
+ transformed_value = get_transformed_value(picked_value, path_context)
100
+ transformed_value = each_value_block.call(transformed_value, path_context)
101
+
102
+ set_mapped_value(mapped_data, key: path_context.to, value: transformed_value)
103
+ end
104
+ mapped_data
105
+ end
106
+
107
+ private
108
+
109
+ def set_mapped_value(mapped_data, key:, value:)
110
+ Rordash::HashUtil.set(mapped_data, key.to_s, value)
111
+ end
112
+
113
+ def get_transformed_value(picked_value, path_context)
114
+ return get_transformed_values(picked_value, path_context) if path_context.iterable_data?
115
+
116
+ transformed_value = transform_value(picked_value, path_context)
117
+ return transformed_value unless transformed_value.nil?
118
+
119
+ get_fallback_value(path_context)
120
+ end
121
+
122
+ def get_fallback_value(path_context)
123
+ return nil unless path_context.fallback?
124
+
125
+ call_proc(path_context.fallback_proc, path_context)
126
+ end
127
+
128
+ def get_transformed_values(value, path_context)
129
+ value = (value || [])
130
+ value = value.reject { |val| path_context.skip_if_proc.call(val) } if path_context.skippable?
131
+ Rordash::HashUtil.deep_symbolize_keys(value).each_with_index.map do |item|
132
+ transform_value(item, path_context)
133
+ end
134
+ end
135
+
136
+ def transform_value(value, path_context)
137
+ return transform_value_with_builder(value, path_context) if path_context.transform_with_builder?
138
+ return call_proc(path_context.transform, value, path_context) if path_context.transformable?
139
+
140
+ value
141
+ end
142
+
143
+ def transform_value_with_builder(value, path_context)
144
+ builder = PathsBuilder.new(parent_path_context: path_context)
145
+ pb = call_proc(path_context.transform, builder, path_context)
146
+ return pb.build_for(value) if pb&.paths?
147
+
148
+ value
149
+ end
150
+
151
+ # rubocop:disable Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
152
+ def value_at(path_context)
153
+ from = path_context.from
154
+ data = path_context.data
155
+
156
+ val = if %w[* .].include?(from)
157
+ data
158
+ elsif from.is_a?(Array)
159
+ Rordash::HashUtil.pick(data, from)
160
+ elsif from.to_sym == :wrapped_source_data
161
+ return path_context.wrapped_source_data
162
+ else
163
+ Rordash::HashUtil.get(data, from)
164
+ end
165
+
166
+ value = if path_context.iterable_data?
167
+ val = [val] unless val.is_a?(Array)
168
+ (val || []).map { |item| apply_defaults(item, path_context) }
169
+ else
170
+ apply_defaults(val, path_context)
171
+ end
172
+
173
+ Rordash::HashUtil.deep_symbolize_keys(value)
174
+ end
175
+ # rubocop:enable Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
176
+
177
+ def apply_defaults(data, path_context)
178
+ return data unless data.is_a?(Hash) && path_context.defaults?
179
+
180
+ path_context.defaults.merge(data)
181
+ end
182
+
183
+ def exec_transform(value, path_context)
184
+ return value unless path_context.transformable?
185
+
186
+ call_proc(path_context.transform, value, path_context)
187
+ end
188
+
189
+ # rubocop:disable Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
190
+ def call_proc(proc, *args, **extra_args)
191
+ return nil unless proc.is_a?(Proc)
192
+
193
+ allowed_proc_args = %i[req opt]
194
+ proc_args_count = proc.parameters.count { |type, _| allowed_proc_args.include?(type) }
195
+
196
+ allowed_kw_args = %i[key keyreq]
197
+ proc_kw_args = proc.parameters.select { |type, _| allowed_kw_args.include?(type) }.map(&:second)
198
+
199
+ without_extra_args = proc_args_count.zero? && proc_kw_args && args.present? && args.first.is_a?(Hash)
200
+ extra_args = without_extra_args ? args.first : extra_args
201
+
202
+ missing_args_count = [proc_args_count - args.count, 0].max
203
+
204
+ found_args = proc_args_count.positive? ? args.take(proc_args_count) : []
205
+ filled_args = found_args.present? ? found_args.fill(nil, found_args.count...missing_args_count + 1) : []
206
+ filled_extra_args = extra_args.slice(*proc_kw_args)
207
+
208
+ if proc_kw_args.present?
209
+ call_proc_with_extra_args(proc, filled_args, filled_extra_args)
210
+ else
211
+ filled_args.present? ? proc.call(*filled_args) : proc.call
212
+ end
213
+ end
214
+ # rubocop:enable Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
215
+
216
+ def call_proc_with_extra_args(proc, args, **extra_args)
217
+ if args.present?
218
+ args.first.is_a?(Hash) ? proc.call(**args.first) : proc.call
219
+
220
+ proc.call(*args, **extra_args)
221
+ else
222
+ proc.call(**extra_args)
223
+ end
224
+ end
225
+ end
226
+
227
+ def keys
228
+ path_context_collection.map(&:to)
229
+ end
230
+ end
231
+ # rubocop:enable Metrics/ClassLength
232
+ end
@@ -0,0 +1,7 @@
1
+ module JsonPath
2
+ class DefaultDataWrapper < SimpleDelegator
3
+ def initialize(data)
4
+ super(data.deep_symbolize_keys)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,135 @@
1
+ module JsonPath
2
+ # rubocop:disable Metrics/ParameterLists,Metrics/PerceivedComplexity
3
+ class PathContext
4
+ COMMON_TRANSFORMS = {
5
+ iso8601: ->(val) { val.is_a?(Date) ? val.iso8601 : val },
6
+ date: lambda do |val|
7
+ val.is_a?(String) ? (defined? Time.zone.parse) && Time.zone.parse(val)&.to_date : val
8
+ rescue ArgumentError
9
+ val
10
+ end
11
+ }.freeze
12
+
13
+ attr_reader :from, :to, :transform, :use_builder, :defaults, :fallback_proc, :skip_if_proc, :builder, :data,
14
+ :source_data, :nested_paths, :nested_data, :mapped_data
15
+
16
+ delegate :data_wrapper_class, to: :builder
17
+
18
+ # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
19
+ def initialize(json_path, paths_builder, iterable_data:, transform:, use_builder:, defaults:, fallback_proc:,
20
+ to: nil, skip_if_proc: nil)
21
+ allowed_transform_types = COMMON_TRANSFORMS.keys
22
+
23
+ raise ArgumentError, '`from` must be filled' if json_path.blank?
24
+
25
+ if (transform.is_a?(Symbol) || transform.is_a?(String)) && COMMON_TRANSFORMS.keys.exclude?(transform.to_sym)
26
+ raise ArgumentError,
27
+ "`transform`: '#{transform}' must be one of #{allowed_transform_types.inspect}"
28
+ end
29
+
30
+ @builder = paths_builder
31
+ @from = json_path.is_a?(Array) ? json_path.map(&:to_s) : json_path.to_s
32
+ @iterable_data = iterable_data
33
+ @nested_paths = paths_builder.nested_paths.dup.freeze
34
+ @to = to.present? ? to : json_path.to_s
35
+ @transform = transform.is_a?(Symbol) ? COMMON_TRANSFORMS[transform] : transform
36
+ @use_builder = use_builder
37
+ @defaults = Rordash::HashUtil.deep_symbolize_keys(defaults || {})
38
+ @fallback_proc = fallback_proc
39
+ @skip_if_proc = skip_if_proc || proc { false }
40
+
41
+ @skip = false
42
+ @nested_data = nil
43
+ end
44
+ # rubocop:enable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
45
+
46
+ def parent
47
+ builder.parent_path_context
48
+ end
49
+
50
+ def wrapped_source_data
51
+ return nil if source_data.nil?
52
+
53
+ @wrapped_source_data ||= data_wrapper_class.new(source_data)
54
+ end
55
+
56
+ def iterable_data?
57
+ @iterable_data
58
+ end
59
+
60
+ def skippable?
61
+ skip_if_proc.is_a?(Proc)
62
+ end
63
+
64
+ def fallback?
65
+ fallback_proc.is_a?(Proc)
66
+ end
67
+
68
+ def nested?
69
+ @nested_paths.present?
70
+ end
71
+
72
+ def defaults?
73
+ @defaults.is_a?(Hash) && @defaults.present?
74
+ end
75
+
76
+ def transform_with_builder?
77
+ !!@use_builder
78
+ end
79
+
80
+ def transformable?
81
+ transform.is_a?(Proc)
82
+ end
83
+
84
+ def unmatched_nested?
85
+ nested? && nested_data.blank?
86
+ end
87
+
88
+ def nested_data?
89
+ @nested_data.present?
90
+ end
91
+
92
+ def with_source_data(data)
93
+ @source_data = data.dup.freeze
94
+ set_nested_data
95
+ set_data
96
+
97
+ self
98
+ end
99
+
100
+ def with_prev_mapped_data(data)
101
+ @mapped_data = data.dup.freeze
102
+ end
103
+
104
+ def to_h
105
+ {
106
+ from: from,
107
+ to: to,
108
+ data: data,
109
+ source_data: source_data,
110
+ nested_data: nested_data,
111
+ mapped_data: mapped_data,
112
+ transform: transform,
113
+ use_builder: use_builder,
114
+ defaults: defaults,
115
+ fallback_proc: fallback_proc,
116
+ skip_if_proc: skip_if_proc
117
+ }
118
+ end
119
+
120
+ private
121
+
122
+ def set_nested_data
123
+ return if @nested_paths.empty?
124
+
125
+ @nested_data = Rordash::HashUtil.get(source_data, @nested_paths.join('.'))
126
+ end
127
+
128
+ def set_data
129
+ @data = nested_data? ? nested_data.dup : source_data.dup
130
+
131
+ self
132
+ end
133
+ end
134
+ # rubocop:enable Metrics/ParameterLists,Metrics/PerceivedComplexity
135
+ end
@@ -0,0 +1,65 @@
1
+ module JsonPath
2
+ # rubocop:disable Metrics/ParameterLists
3
+ class PathContextCollection < SimpleDelegator
4
+ ROOT_PATHS = %w[* .].freeze
5
+
6
+ attr_reader :source_data, :builder
7
+
8
+ def initialize(paths_builder)
9
+ @builder = paths_builder
10
+ @source_data = nil
11
+ @nested_paths = []
12
+
13
+ super([])
14
+ end
15
+
16
+ def reject_from_paths!(from_paths)
17
+ reject! { |path_context| from_paths.include?(path_context.from.to_s) }
18
+ end
19
+
20
+ def data_wrapper_class
21
+ @data_wrapper_class || DefaultDataWrapper
22
+ end
23
+
24
+ def nested_paths
25
+ @nested_paths.reject { |p| p.blank? || ROOT_PATHS.include?(p) }
26
+ end
27
+
28
+ def within(json_path)
29
+ @nested_paths.push(json_path)
30
+ yield builder
31
+ @nested_paths.pop
32
+ end
33
+
34
+ def add_path(path, paths_builder,
35
+ iterable_data:,
36
+ transform:,
37
+ defaults:,
38
+ fallback_proc:,
39
+ skip_if_proc:,
40
+ to: nil,
41
+ use_builder: true)
42
+ push(PathContext.new(path, paths_builder,
43
+ to: to,
44
+ iterable_data: iterable_data,
45
+ transform: transform,
46
+ use_builder: use_builder,
47
+ defaults: defaults,
48
+ fallback_proc: fallback_proc,
49
+ skip_if_proc: skip_if_proc))
50
+ self
51
+ end
52
+
53
+ def with_source_data(data)
54
+ @source_data = data
55
+
56
+ each { |path| path.with_source_data(data) }
57
+ self
58
+ end
59
+
60
+ def with_wrapped_data_class(klass)
61
+ @data_wrapper_class = klass
62
+ end
63
+ end
64
+ # rubocop:enable Metrics/ParameterLists
65
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonPathBuilder
4
+ VERSION = "0.1.0"
5
+ end
data/lib/json_path.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rordash'
4
+
5
+ %w[
6
+ version
7
+ default_data_wrapper
8
+ path_context
9
+ path_context_collection
10
+ builder
11
+ ].each do |filename|
12
+ require File.expand_path("../json-path/#{filename}", Pathname.new(__FILE__).realpath)
13
+ end
14
+
15
+ module JsonPath; end
@@ -0,0 +1,4 @@
1
+ module JsonPathBuilder
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,12 @@
1
+ if ENV['RUN_COVERAGE_REPORT']
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start do
5
+ add_filter 'vendor/'
6
+ add_filter %r{^/spec/}
7
+ end
8
+ SimpleCov.minimum_coverage_by_file 90
9
+
10
+ require 'codecov'
11
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
12
+ end