code-ruby 3.0.2 → 3.0.4

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: b61a40e84c1815a9437f6127c4db28d7327982671aa1a602e11ccc85e467861f
4
- data.tar.gz: 796e46b1fbd2443799bd4cba9fcd7513c462bc9b605d31d17a2d1197ee7d6fd1
3
+ metadata.gz: 5a1d60879ca94a53452e814d8c8de4975be88088beb0ce216f3584a48b0dfcae
4
+ data.tar.gz: 0ad78db73acc96f99df57e3c945c5d97fb404ed692d5f9de7c7bc08c25562468
5
5
  SHA512:
6
- metadata.gz: 4d131863a968fa7e02711182c37840bd7743770d32da91f031bdc8e6ccd42aedea8312c1521c1095205eca46ed87f2b663a0622cc97706beb6383b5998bba097
7
- data.tar.gz: 9b17fa0e159d1472929fd284020e538937ed1465cfeb0e36dce1f079044e657920edfd2d000aedee61a2e2a9f5eaabdf3ac3503833d162d3693efcccc594ffbd
6
+ metadata.gz: 2eec31b74d22e1d123ea138cf41063792cbff5e2192e26a692c7dfaa52834db9112bacb9929d3a88d59a6fd4296cc23ff0501a41761f95fa963b027878eda54c
7
+ data.tar.gz: 3363b4f05ca9203842aa8c3a6eee0ccd2ac570609cd9bf3bfb77bda134b234c2ddd0e95a9265941ac8239659260823dfc8e36c8fb34643d835cc702dbc5e5ae4
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- code-ruby (3.0.2)
4
+ code-ruby (3.0.4)
5
5
  activesupport
6
6
  base64
7
7
  bigdecimal
8
8
  did-you-mean
9
9
  dorian-arguments
10
+ icalendar
10
11
  json
11
12
  language-ruby
12
13
  mail
@@ -95,6 +96,12 @@ GEM
95
96
  strscan (>= 3.1.2)
96
97
  i18n (1.14.8)
97
98
  concurrent-ruby (~> 1.0)
99
+ icalendar (2.12.2)
100
+ base64
101
+ ice_cube (~> 0.16)
102
+ logger
103
+ ostruct
104
+ ice_cube (0.17.0)
98
105
  json (2.18.1)
99
106
  json-schema (6.1.0)
100
107
  addressable (~> 2.8)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.0.4
data/code-ruby.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.add_dependency "bigdecimal"
19
19
  s.add_dependency "did-you-mean"
20
20
  s.add_dependency "dorian-arguments"
21
+ s.add_dependency "icalendar"
21
22
  s.add_dependency "json"
22
23
  s.add_dependency "language-ruby"
23
24
  s.add_dependency "mail"
@@ -167,6 +167,9 @@ class Code
167
167
  when "Json"
168
168
  sig(args) { Object.repeat }
169
169
  code_arguments.any? ? Json.new(*code_arguments.raw) : Class.new(Json)
170
+ when "Ics"
171
+ sig(args) { Object.repeat }
172
+ code_arguments.any? ? Ics.new(*code_arguments.raw) : Class.new(Ics)
170
173
  when "evaluate"
171
174
  sig(args) { Object }
172
175
  Code.code_evaluate(code_value.code_to_string, **globals)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Code
4
+ class Object
5
+ class Ics < Object
6
+ EVENT_ATTRIBUTES = %i[
7
+ uid
8
+ summary
9
+ description
10
+ location
11
+ url
12
+ status
13
+ organizer
14
+ categories
15
+ attendees
16
+ geo
17
+ ].freeze
18
+
19
+ def self.call(**args)
20
+ code_operator = args.fetch(:operator, nil).to_code
21
+ code_arguments = args.fetch(:arguments, []).to_code
22
+ code_value = code_arguments.code_first
23
+
24
+ case code_operator.to_s
25
+ when "parse"
26
+ sig(args) { String }
27
+ code_parse(code_value)
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def self.code_parse(value)
34
+ source = value.to_code.raw
35
+ calendars = ::Icalendar::Calendar.parse(source)
36
+ calendars
37
+ .flat_map(&:events)
38
+ .map { |event| serialize_event(event) }
39
+ .to_code
40
+ rescue StandardError
41
+ [].to_code
42
+ end
43
+
44
+ def self.serialize_event(event)
45
+ EVENT_ATTRIBUTES.each_with_object({}) do |attribute, result|
46
+ next unless event.respond_to?(attribute)
47
+
48
+ serialized = serialize_value(event.public_send(attribute))
49
+ serialized =
50
+ if attribute == :categories && serialized.is_a?(::Array)
51
+ serialized.flatten(1)
52
+ elsif scalar_event_attribute?(attribute) && serialized.is_a?(::Array)
53
+ serialized.join(",")
54
+ else
55
+ serialized
56
+ end
57
+ result[attribute] = serialized unless serialized.nil?
58
+ end.merge(
59
+ timestamp: serialize_value(event.dtstamp),
60
+ starts_at: serialize_value(event.dtstart),
61
+ ends_at: serialize_value(event.dtend),
62
+ all_day: !!serialize_date_like(event.dtstart).is_a?(::Date)
63
+ ).compact
64
+ end
65
+
66
+ def self.serialize_value(value)
67
+ case value
68
+ when nil
69
+ nil
70
+ when ::String
71
+ normalize_string(value)
72
+ when ::Symbol, ::Integer, ::Float, ::BigDecimal, true, false
73
+ value
74
+ when ::Array
75
+ value.map { |item| serialize_value(item) }
76
+ when ::Hash
77
+ value.transform_values { |item| serialize_value(item) }
78
+ else
79
+ serialized_date = serialize_date_like(value)
80
+ return serialized_date unless serialized_date.nil?
81
+
82
+ if value.respond_to?(:value)
83
+ serialize_value(value.value)
84
+ elsif value.respond_to?(:to_ical)
85
+ normalize_string(value.to_ical)
86
+ else
87
+ normalize_string(value.to_s)
88
+ end
89
+ end
90
+ end
91
+
92
+ def self.scalar_event_attribute?(attribute)
93
+ !%i[categories attendees geo].include?(attribute)
94
+ end
95
+
96
+ def self.normalize_string(value)
97
+ value
98
+ .dup
99
+ .force_encoding(::Encoding::UTF_8)
100
+ .encode(::Encoding::UTF_8, invalid: :replace, undef: :replace)
101
+ end
102
+
103
+ def self.serialize_date_like(value)
104
+ case value
105
+ when ::Time, ::Date, ::ActiveSupport::TimeWithZone
106
+ value
107
+ when ::DateTime
108
+ value.to_time
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -74,7 +74,7 @@ class Code
74
74
  sig(args)
75
75
  code_strip
76
76
  when "split"
77
- sig(args) { String }
77
+ sig(args) { String.maybe }
78
78
  code_split(code_value)
79
79
  else
80
80
  super
@@ -162,7 +162,11 @@ class Code
162
162
  def code_split(value)
163
163
  code_value = value.to_code
164
164
 
165
- List.new(raw.split(code_value.to_s))
165
+ if code_value.nothing?
166
+ List.new(raw.split)
167
+ else
168
+ List.new(raw.split(code_value.to_s))
169
+ end
166
170
  end
167
171
 
168
172
  def present?
data/lib/code-ruby.rb CHANGED
@@ -7,6 +7,7 @@ require "bigdecimal/util"
7
7
  require "date"
8
8
  require "did_you_mean"
9
9
  require "digest"
10
+ require "icalendar"
10
11
  require "json"
11
12
  require "mail"
12
13
  require "net/http"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Code::Object::Ics do
6
+ it "normalizes binary event strings before json serialization" do
7
+ source =
8
+ <<~ICS
9
+ BEGIN:VCALENDAR
10
+ VERSION:2.0
11
+ PRODID:-//code-ruby//EN
12
+ BEGIN:VEVENT
13
+ UID:event-1
14
+ DTSTART:20260327T120000Z
15
+ DTEND:20260327T133000Z
16
+ SUMMARY:Joséphine
17
+ DESCRIPTION:https://luma.com/opv3owre
18
+ END:VEVENT
19
+ END:VCALENDAR
20
+ ICS
21
+
22
+ events = described_class.code_parse(Code::Object::String.new(source.b))
23
+
24
+ expect { expect(events.to_json).to include("Joséphine") }.not_to output.to_stderr
25
+ end
26
+
27
+ it "serializes comma-separated descriptions as strings" do
28
+ source =
29
+ <<~ICS
30
+ BEGIN:VCALENDAR
31
+ VERSION:2.0
32
+ PRODID:-//code-ruby//EN
33
+ BEGIN:VEVENT
34
+ UID:event-2
35
+ DTSTART:20260401T170000Z
36
+ DTEND:20260401T190000Z
37
+ SUMMARY:Le Cirque du Donut
38
+ DESCRIPTION:Bonjour Paris !\\nLe 1er avril, Kate Raworth vous donne rendez-vous, pour une soirée inédite.
39
+ END:VEVENT
40
+ END:VCALENDAR
41
+ ICS
42
+
43
+ events = described_class.code_parse(Code::Object::String.new(source))
44
+ description = events.raw.first.code_get("description").raw
45
+
46
+ expect(description).to be_a(String)
47
+ expect(description).to include("Le 1er avril, Kate Raworth")
48
+ expect(description).to include("rendez-vous, pour")
49
+ end
50
+ end
data/spec/code_spec.rb CHANGED
@@ -151,6 +151,7 @@ RSpec.describe Code do
151
151
  Json.parse('[]')
152
152
  Json.parse('{}')
153
153
  Json.parse('random-string')
154
+ Ics.parse("BEGIN:VCALENDAR\nEND:VCALENDAR")
154
155
  {}["".to_string]
155
156
  ] + ["Time.hour >= 6 and Time.hour <= 23"]
156
157
  ).each do |input|
@@ -557,4 +558,51 @@ RSpec.describe Code do
557
558
  described_class.evaluate("UnknownConstant.zone = 1")
558
559
  end.to raise_error(Code::Error, /UnknownConstant is not defined/)
559
560
  end
561
+
562
+ it "parses ics events into dictionaries" do
563
+ result = described_class.evaluate(<<~CODE)
564
+ events = Ics.parse("BEGIN:VCALENDAR
565
+ VERSION:2.0
566
+ PRODID:-//code-ruby//EN
567
+ BEGIN:VEVENT
568
+ UID:event-1
569
+ DTSTART:20260327T120000Z
570
+ DTEND:20260327T133000Z
571
+ SUMMARY:Lunch
572
+ DESCRIPTION:Team lunch
573
+ LOCATION:Paris
574
+ URL:https://example.com/events/1
575
+ CATEGORIES:food,team
576
+ END:VEVENT
577
+ END:VCALENDAR")
578
+
579
+ {
580
+ size: events.size,
581
+ summary: events.first.summary,
582
+ location: events.first.location,
583
+ uid: events.first.uid,
584
+ categories: events.first.categories,
585
+ starts_at: events.first.starts_at.to_string,
586
+ ends_at: events.first.ends_at.to_string,
587
+ all_day: events.first.all_day
588
+ }
589
+ CODE
590
+
591
+ expect(result).to eq(
592
+ {
593
+ size: 1,
594
+ summary: "Lunch",
595
+ location: "Paris",
596
+ uid: "event-1",
597
+ categories: ["food", "team"],
598
+ starts_at: "2026-03-27 12:00:00 UTC",
599
+ ends_at: "2026-03-27 13:30:00 UTC",
600
+ all_day: false
601
+ }.to_code
602
+ )
603
+ end
604
+
605
+ it "returns an empty list for invalid ics" do
606
+ expect(described_class.evaluate('Ics.parse("not an ics file")')).to eq([].to_code)
607
+ end
560
608
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian Marié
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: icalendar
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: json
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -275,6 +289,7 @@ files:
275
289
  - lib/code/object/global.rb
276
290
  - lib/code/object/html.rb
277
291
  - lib/code/object/http.rb
292
+ - lib/code/object/ics.rb
278
293
  - lib/code/object/identifier_list.rb
279
294
  - lib/code/object/integer.rb
280
295
  - lib/code/object/json.rb
@@ -306,6 +321,7 @@ files:
306
321
  - spec/code/object/decimal_spec.rb
307
322
  - spec/code/object/dictionary_spec.rb
308
323
  - spec/code/object/function_spec.rb
324
+ - spec/code/object/ics_spec.rb
309
325
  - spec/code/object/integer_spec.rb
310
326
  - spec/code/object/list_spec.rb
311
327
  - spec/code/object/nothing_spec.rb