opdotenv 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd66207d97fd6cf74fe9eaf05dcd22e43b05dd68af9995dcc5348be00fd465ae
4
- data.tar.gz: 78f719f95dcd42edbdde3e00ed0be0ca9be5ef470055e0a6e49623f3599fd566
3
+ metadata.gz: 681b84ce04a08dea4ef5bb4645ecf47071922f63288f5d6b26fcc558d2b181fd
4
+ data.tar.gz: d6706fb654448e9d4efa404d373d54d5f9ef5b5f5553a73c5335c28b6a7f9508
5
5
  SHA512:
6
- metadata.gz: 4ba1fbc82986e6a20c8302c21bf27e0c61a3ba4703379dc0a9966d115d0101c8383497a7eea4a14cbce010eff8a94d273f98a6da1c5c01810773d2dc70c6400a
7
- data.tar.gz: f695a8cf1a6e7e1f3c2daba02609366618b82f66162c45a4f64912e50dae71313dfab2cc0cd276dfae8409f43ba6895707a3a85b9447eaea4af00653bebd82e6
6
+ metadata.gz: 43741125e5322870b2f11d57d6b7c2a1d6ea695d69209b93938a546b5ac44f785064b21cdaf2ca6ead5a232b5e36d6d0035f90a4fa9e2f668af0b53aa1c34f8d
7
+ data.tar.gz: 0b1855b8e5804b98dd8bf4936a218a69bfb415073e38b78f25102744b979674f9afda9baec9570c4dbf67deeee46e226be78073ab9a51105f9e989e9a2f8a522
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.5 (2026-07-06)
4
+
5
+ - Improve `OPDOTENV_DEBUG` field summary logging in Anyway Config integration to build messages only when Rails debug logging is active
6
+ - Skip field summary debug output when a 1Password item has no fields
7
+
8
+ ## 1.0.4 (2026-07-05)
9
+
10
+ - Move `railties` from runtime to development dependency in gemspec
11
+ - Non-Rails installs no longer pull in the Rails gem tree; Rails apps already provide `railties`
12
+
3
13
  ## 1.0.3 (2025-11-07)
4
14
 
5
15
  - Fix exit code handling in OpClient by using Open3.capture2e instead of IO.popen for reliable process status
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # opdotenv
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/opdotenv.svg?v=1.0.3)](https://badge.fury.io/rb/opdotenv) [![Test Status](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/opdotenv.rb/graph/badge.svg?token=U4FMVZGO8R)](https://codecov.io/gh/amkisko/opdotenv.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/opdotenv.svg?v=1.0.3)](https://badge.fury.io/rb/opdotenv) [![Test Status](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/opdotenv.rb/graph/badge.svg?token=U4FMVZGO8R)](https://codecov.io/gh/amkisko/opdotenv.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_opdotenv.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_opdotenv.rb)
4
4
 
5
5
  Load environment variables from 1Password using the `op` CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.
6
6
 
@@ -350,6 +350,24 @@ bundle exec rbs validate
350
350
  bundle exec standardrb --fix
351
351
  ```
352
352
 
353
+ ## Contributing
354
+
355
+ Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/opdotenv.rb.
356
+
357
+ Contribution policy:
358
+ - New features are not necessarily added to the gem
359
+ - Pull request should have test coverage for affected parts
360
+ - Pull request should have changelog entry
361
+
362
+ Review policy:
363
+ - It might take up to 2 calendar weeks to review and merge critical fixes
364
+ - It might take up to 6 calendar months to review and merge pull request
365
+ - It might take up to 1 calendar year to review an issue
366
+
367
+ ## Security
368
+
369
+ If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
370
+
353
371
  ## License
354
372
 
355
- MIT
373
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -79,26 +79,38 @@ module Opdotenv
79
79
  def log_available_fields(original_data, env_prefix, matched_data)
80
80
  return unless ENV["OPDOTENV_DEBUG"] == "true"
81
81
  return unless defined?(Rails) && Rails.logger
82
+ return if original_data.keys.empty?
82
83
 
83
84
  prefix_upper = env_prefix.upcase
84
85
  prefix_with_underscore = "#{prefix_upper}_"
85
-
86
86
  available_fields = original_data.keys.map(&:to_s)
87
87
  matched_fields = matched_data.keys.map(&:to_s)
88
+ unmatched = unmatched_fields(available_fields, prefix_upper, prefix_with_underscore)
89
+
90
+ log_field_summary(available_fields, matched_fields, env_prefix, prefix_with_underscore)
91
+ log_unmatched_fields(unmatched, prefix_with_underscore) if unmatched.any?
92
+ end
88
93
 
89
- # Find fields that were available but didn't match the prefix (case-insensitive)
90
- unmatched = available_fields.reject do |field|
94
+ def unmatched_fields(available_fields, prefix_upper, prefix_with_underscore)
95
+ available_fields.reject do |field|
91
96
  field_upper = field.to_s.upcase
92
97
  field_upper.start_with?(prefix_with_underscore) || field_upper == prefix_upper
93
98
  end
99
+ end
94
100
 
95
- if available_fields.any?
96
- Rails.logger.debug("[opdotenv] Available fields from 1Password: #{available_fields.join(", ")}")
97
- Rails.logger.debug("[opdotenv] Matched fields for #{env_prefix} (prefixed with #{prefix_with_underscore}, case-insensitive): #{matched_fields.join(", ")}")
98
- if unmatched.any?
99
- Rails.logger.debug("[opdotenv] Unmatched fields (must be prefixed with #{prefix_with_underscore}, case-insensitive): #{unmatched.join(", ")}")
100
- Rails.logger.debug("[opdotenv] To use these fields, rename them in 1Password to include the #{prefix_with_underscore} prefix")
101
- end
101
+ def log_field_summary(available_fields, matched_fields, env_prefix, prefix_with_underscore)
102
+ Rails.logger.debug { "[opdotenv] Available fields from 1Password: #{available_fields.join(", ")}" }
103
+ Rails.logger.debug do
104
+ "[opdotenv] Matched fields for #{env_prefix} (prefixed with #{prefix_with_underscore}, case-insensitive): #{matched_fields.join(", ")}"
105
+ end
106
+ end
107
+
108
+ def log_unmatched_fields(unmatched, prefix_with_underscore)
109
+ Rails.logger.debug do
110
+ "[opdotenv] Unmatched fields (must be prefixed with #{prefix_with_underscore}, case-insensitive): #{unmatched.join(", ")}"
111
+ end
112
+ Rails.logger.debug do
113
+ "[opdotenv] To use these fields, rename them in 1Password to include the #{prefix_with_underscore} prefix"
102
114
  end
103
115
  end
104
116
  end
@@ -86,28 +86,7 @@ module Opdotenv
86
86
  existing = item_by_title_in_vault(vault_id, item)
87
87
 
88
88
  if existing
89
- # Update using PATCH
90
- fields_array = fields.map do |k, v|
91
- existing_field = existing["fields"]&.find { |f| f["label"] == k.to_s }
92
- if existing_field
93
- {
94
- "op" => "replace",
95
- "path" => "/fields/#{existing_field["id"]}/value",
96
- "value" => v.to_s
97
- }
98
- else
99
- {
100
- "op" => "add",
101
- "path" => "/fields",
102
- "value" => {
103
- "type" => "CONCEALED",
104
- "label" => k.to_s,
105
- "value" => v.to_s
106
- }
107
- }
108
- end
109
- end
110
-
89
+ fields_array = build_patch_fields(existing, fields)
111
90
  api_request(:patch, "/v1/vaults/#{vault_id}/items/#{existing["id"]}", fields_array)
112
91
  else
113
92
  # Create new item
@@ -132,6 +111,29 @@ module Opdotenv
132
111
 
133
112
  private
134
113
 
114
+ def build_patch_fields(existing, fields)
115
+ fields.map do |key, value|
116
+ existing_field = existing["fields"]&.find { |field| field["label"] == key.to_s }
117
+ if existing_field
118
+ {
119
+ "op" => "replace",
120
+ "path" => "/fields/#{existing_field["id"]}/value",
121
+ "value" => value.to_s
122
+ }
123
+ else
124
+ {
125
+ "op" => "add",
126
+ "path" => "/fields",
127
+ "value" => {
128
+ "type" => "CONCEALED",
129
+ "label" => key.to_s,
130
+ "value" => value.to_s
131
+ }
132
+ }
133
+ end
134
+ end
135
+ end
136
+
135
137
  def parse_path(path)
136
138
  # op://Vault/Item or connect://Vault/Item
137
139
  match = path.match(/\A(?:op|connect):\/\/([^\/]+)\/([^\/]+)(?:\/(.+))?\z/)
@@ -25,24 +25,25 @@ module Opdotenv
25
25
  raw_json = client.item_get(item, vault: vault)
26
26
  item_hash = parse_json_safe(raw_json)
27
27
 
28
- # Only process fields from the 1Password item, filter out empty values
29
28
  item_hash["fields"]&.each_with_object({}) do |field, env_data|
30
- # Only include actual fields from the item
31
- next unless field.is_a?(Hash)
29
+ entry = field_env_entry(field)
30
+ next unless entry
32
31
 
33
- # Strip whitespace from label and treat empty strings as nil
34
- label = (field["label"] || field["id"])&.strip
35
- next if label.nil? || label.empty?
32
+ env_data[entry[:key]] = entry[:value]
33
+ end || {}
34
+ end
36
35
 
37
- # Skip notesPlain when fetching all fields
38
- next if field["purpose"] == NOTES_PURPOSE
36
+ def self.field_env_entry(field)
37
+ return nil unless field.is_a?(Hash)
39
38
 
40
- # Get the value and filter out empty values
41
- value = field["value"]
42
- next if value.nil? || (value.is_a?(String) && value.strip.empty?)
39
+ label = (field["label"] || field["id"])&.strip
40
+ return nil if label.nil? || label.empty?
41
+ return nil if field["purpose"] == NOTES_PURPOSE
43
42
 
44
- env_data[label.to_s] = value.to_s
45
- end || {}
43
+ value = field["value"]
44
+ return nil if value.nil? || (value.is_a?(String) && value.strip.empty?)
45
+
46
+ {key: label.to_s, value: value.to_s}
46
47
  end
47
48
 
48
49
  def self.build_field_path(path, field_name)
@@ -2,26 +2,33 @@ module Opdotenv
2
2
  module Parsers
3
3
  class DotenvParser
4
4
  def self.parse(text)
5
- env = {}
6
- text.to_s.each_line do |line|
7
- line = line.strip
8
- next if line.empty? || line.start_with?("#")
9
- # Support KEY=VALUE and KEY="VALUE"; ignore export prefix
10
- line = line.sub(/^export\s+/, "")
11
- if (m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\z/))
12
- key = m[1]
13
- raw = m[2]
14
- value = if raw.start_with?("\"") && raw.end_with?("\"")
15
- raw[1..-2].gsub('\\"', '"')
16
- elsif raw.start_with?("'") && raw.end_with?("'")
17
- raw[1..-2]
18
- else
19
- raw
20
- end
21
- env[key] = value
22
- end
5
+ text.to_s.each_line.with_object({}) do |line, env|
6
+ entry = parse_line(line)
7
+ next unless entry
8
+
9
+ env[entry[:key]] = entry[:value]
10
+ end
11
+ end
12
+
13
+ def self.parse_line(line)
14
+ line = line.strip
15
+ return nil if line.empty? || line.start_with?("#")
16
+
17
+ line = line.sub(/^export\s+/, "")
18
+ match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\z/)
19
+ return nil unless match
20
+
21
+ {key: match[1], value: unquote_value(match[2])}
22
+ end
23
+
24
+ def self.unquote_value(raw)
25
+ if raw.start_with?("\"") && raw.end_with?("\"")
26
+ raw[1..-2].gsub('\\"', '"')
27
+ elsif raw.start_with?("'") && raw.end_with?("'")
28
+ raw[1..-2]
29
+ else
30
+ raw
23
31
  end
24
- env
25
32
  end
26
33
  end
27
34
  end
@@ -19,18 +19,17 @@ module Opdotenv
19
19
 
20
20
  path = source.to_s
21
21
  _, item = parse_op_path(path)
22
-
23
- # Extract item name and potential field name
24
- # Handle paths like "op://Vault/Item" or "op://Vault/Item Name/field"
25
22
  item_parts = item.split("/")
26
23
  item_name = item_parts.first
27
24
  field_name = (item_parts.length > 1) ? item_parts[1] : nil
28
25
 
29
- # First check if field name is provided with extension in the path
26
+ build_parsed_source(path, item_name, field_name)
27
+ end
28
+
29
+ def build_parsed_source(path, item_name, field_name)
30
30
  if field_name
31
31
  field_type = FormatInferrer.infer_from_name(field_name)
32
32
  {path: path, field_name: field_name, field_type: field_type}
33
- # If no field name in path, infer from item name patterns
34
33
  elsif (field_type = FormatInferrer.infer_from_name(item_name))
35
34
  {path: path, field_name: NOTES_PLAIN_FIELD, field_type: field_type}
36
35
  else
@@ -66,6 +65,6 @@ module Opdotenv
66
65
  }
67
66
  end
68
67
 
69
- module_function :normalize_hash
68
+ module_function :normalize_hash, :build_parsed_source
70
69
  end
71
70
  end
@@ -1,3 +1,3 @@
1
1
  module Opdotenv
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opdotenv
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -19,7 +19,7 @@ dependencies:
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '9.0'
22
- type: :runtime
22
+ type: :development
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '3'
60
+ - !ruby/object:Gem::Dependency
61
+ name: polyrun
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: 1.5.0
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.5.0
60
74
  - !ruby/object:Gem::Dependency
61
75
  name: simplecov
62
76
  requirement: !ruby/object:Gem::Requirement
@@ -105,28 +119,112 @@ dependencies:
105
119
  requirements:
106
120
  - - "~>"
107
121
  - !ruby/object:Gem::Version
108
- version: '1'
122
+ version: '1.52'
109
123
  type: :development
110
124
  prerelease: false
111
125
  version_requirements: !ruby/object:Gem::Requirement
112
126
  requirements:
113
127
  - - "~>"
114
128
  - !ruby/object:Gem::Version
115
- version: '1'
129
+ version: '1.52'
130
+ - !ruby/object:Gem::Dependency
131
+ name: standard-custom
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '1.0'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '1.0'
116
144
  - !ruby/object:Gem::Dependency
117
145
  name: standard-performance
118
146
  requirement: !ruby/object:Gem::Requirement
119
147
  requirements:
120
148
  - - "~>"
121
149
  - !ruby/object:Gem::Version
122
- version: '1'
150
+ version: '1.8'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '1.8'
158
+ - !ruby/object:Gem::Dependency
159
+ name: standard-rails
160
+ requirement: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '1.5'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '1.5'
172
+ - !ruby/object:Gem::Dependency
173
+ name: standard-rspec
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '0.3'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '0.3'
186
+ - !ruby/object:Gem::Dependency
187
+ name: rubocop-rails
188
+ requirement: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - "~>"
191
+ - !ruby/object:Gem::Version
192
+ version: '2.33'
193
+ type: :development
194
+ prerelease: false
195
+ version_requirements: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - "~>"
198
+ - !ruby/object:Gem::Version
199
+ version: '2.33'
200
+ - !ruby/object:Gem::Dependency
201
+ name: rubocop-rspec
202
+ requirement: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - "~>"
205
+ - !ruby/object:Gem::Version
206
+ version: '3.8'
207
+ type: :development
208
+ prerelease: false
209
+ version_requirements: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - "~>"
212
+ - !ruby/object:Gem::Version
213
+ version: '3.8'
214
+ - !ruby/object:Gem::Dependency
215
+ name: rubocop-thread_safety
216
+ requirement: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - "~>"
219
+ - !ruby/object:Gem::Version
220
+ version: '0.7'
123
221
  type: :development
124
222
  prerelease: false
125
223
  version_requirements: !ruby/object:Gem::Requirement
126
224
  requirements:
127
225
  - - "~>"
128
226
  - !ruby/object:Gem::Version
129
- version: '1'
227
+ version: '0.7'
130
228
  - !ruby/object:Gem::Dependency
131
229
  name: appraisal
132
230
  requirement: !ruby/object:Gem::Requirement