json-path-builder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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