api_record 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b539925125802f0bbf668f5f5feb22cb89d7c1e1907a9e61b66550bd5e05534c
4
+ data.tar.gz: d277bb682de2964c9101776cf8d871d419e3477ab361412e1e5da62a6489f36f
5
+ SHA512:
6
+ metadata.gz: e687a8e8738f278a8fed2734181b2d568432c2c25f9511d58def209378265cae525c3745131e03c91b7f8af189f18a6dd902f4c494397e8777eb58159603922a
7
+ data.tar.gz: 2d6399dfd008939dedd3d2cd57599abca8855db5224c5b6d59d3b62025803b1d6a1c6560e69de765775db3e32f4881a4d27a60235f46e00aaaefb34f80113762
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in api_record.gemspec
6
+ gemspec
7
+
8
+
9
+ group :development, :test do
10
+ gem "webmock", "~> 3.8"
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ api_record (0.0.1)
5
+ activemodel (>= 6, < 7.2)
6
+ faraday (>= 2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (7.1.2)
12
+ activesupport (= 7.1.2)
13
+ activesupport (7.1.2)
14
+ base64
15
+ bigdecimal
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ connection_pool (>= 2.2.5)
18
+ drb
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ mutex_m
22
+ tzinfo (~> 2.0)
23
+ addressable (2.8.6)
24
+ public_suffix (>= 2.0.2, < 6.0)
25
+ base64 (0.2.0)
26
+ bigdecimal (3.1.5)
27
+ concurrent-ruby (1.2.2)
28
+ connection_pool (2.4.1)
29
+ crack (0.4.5)
30
+ rexml
31
+ diff-lcs (1.5.0)
32
+ drb (2.2.0)
33
+ ruby2_keywords
34
+ faraday (2.8.1)
35
+ base64
36
+ faraday-net_http (>= 2.0, < 3.1)
37
+ ruby2_keywords (>= 0.0.4)
38
+ faraday-net_http (3.0.2)
39
+ hashdiff (1.1.0)
40
+ i18n (1.14.1)
41
+ concurrent-ruby (~> 1.0)
42
+ minitest (5.20.0)
43
+ mutex_m (0.2.0)
44
+ public_suffix (5.0.4)
45
+ rake (13.1.0)
46
+ rexml (3.2.6)
47
+ rspec (3.12.0)
48
+ rspec-core (~> 3.12.0)
49
+ rspec-expectations (~> 3.12.0)
50
+ rspec-mocks (~> 3.12.0)
51
+ rspec-core (3.12.2)
52
+ rspec-support (~> 3.12.0)
53
+ rspec-expectations (3.12.3)
54
+ diff-lcs (>= 1.2.0, < 2.0)
55
+ rspec-support (~> 3.12.0)
56
+ rspec-mocks (3.12.6)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.12.0)
59
+ rspec-support (3.12.1)
60
+ ruby2_keywords (0.0.5)
61
+ tzinfo (2.0.6)
62
+ concurrent-ruby (~> 1.0)
63
+ webmock (3.19.1)
64
+ addressable (>= 2.8.0)
65
+ crack (>= 0.3.2)
66
+ hashdiff (>= 0.4.0, < 2.0.0)
67
+
68
+ PLATFORMS
69
+ x86_64-linux
70
+
71
+ DEPENDENCIES
72
+ api_record!
73
+ bundler
74
+ rake
75
+ rspec
76
+ webmock (~> 3.8)
77
+
78
+ BUNDLED WITH
79
+ 2.2.32
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Takuya Nishio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # ApiRecord
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/api_record`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'api_record'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install api_record
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/api_record.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: %i[spec]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "api_record"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
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
@@ -0,0 +1,19 @@
1
+ require 'active_support/all'
2
+ require "api_record/base"
3
+ require "api_record/destroyable"
4
+ require "api_record/errors"
5
+ require "api_record/findable"
6
+ require "api_record/indexable"
7
+ require "api_record/saveable"
8
+
9
+ module ApiRecord
10
+ module All
11
+ extend ActiveSupport::Concern
12
+
13
+ include ApiRecord::Base
14
+ include ApiRecord::Findable
15
+ include ApiRecord::Indexable
16
+ include ApiRecord::Saveable
17
+ include ApiRecord::Destroyable
18
+ end
19
+ end
@@ -0,0 +1,155 @@
1
+ require "active_model"
2
+ require 'active_support/all'
3
+ require "faraday"
4
+ require_relative 'errors'
5
+
6
+ module ApiRecord
7
+ module Base
8
+ extend ActiveSupport::Concern
9
+ include ActiveModel::Model
10
+ include ActiveModel::Attributes
11
+ include ActiveModel::Serializers::JSON
12
+
13
+ included do
14
+ private
15
+
16
+ attr_accessor :response
17
+ end
18
+
19
+ class_methods do
20
+ def api_url
21
+ 'http://localhost:3000'
22
+ end
23
+
24
+ def client
25
+ ::Faraday.new(url: api_url) do |faraday|
26
+ configure_faraday(faraday)
27
+ end
28
+ end
29
+
30
+ def configure_faraday(faraday)
31
+ default_config.call(faraday)
32
+ @config_f.call(faraday) if @config_f
33
+ end
34
+
35
+ def default_config
36
+ lambda do |faraday|
37
+ faraday.adapter ::Faraday.default_adapter
38
+ faraday.request :json
39
+ # faraday.response :logger unless Rails.env.test?
40
+ faraday.response :json
41
+ end
42
+ end
43
+
44
+ def faraday_configure(&block)
45
+ @config_f = block
46
+ end
47
+
48
+ def raise_api_exception(response)
49
+ case response.status
50
+ when 404
51
+ raise ApiNotFound.new(response), "Record not found"
52
+ when 422
53
+ raise ApiInvalidError.new(response), "Invalid data"
54
+ when 400..499
55
+ raise HttpClientError.new(response), "Client error"
56
+ when 500..599
57
+ raise HttpServerError.new(response), "Server error"
58
+ else
59
+ raise ApiError.new(response), "Api Error"
60
+ end
61
+ end
62
+
63
+ def request(method, *args, &block)
64
+ response = client.public_send(method, *args) do |req|
65
+ yield req if block_given?
66
+ end
67
+ if response.success?
68
+ response
69
+ else
70
+ raise_api_exception(response)
71
+ end
72
+ end
73
+ end
74
+
75
+ def inspect
76
+ attributes_to_display = self.class.attribute_names.map(&:to_s) + ['response']
77
+ attributes_string = attributes_to_display.map { |attr| "#{attr}: #{send(attr).inspect}" }.join(', ')
78
+ "#<#{self.class.name} #{attributes_string}>"
79
+ end
80
+
81
+ def has_error?
82
+ errors.any?
83
+ end
84
+
85
+ def validate!
86
+ raise RecordInvalidError.new(self) unless valid?
87
+ end
88
+
89
+ private
90
+
91
+ def request(method, *args, &block)
92
+ @response = self.class.client.public_send(method, *args) do |req|
93
+ yield req if block_given?
94
+ end
95
+ if response.success?
96
+ true
97
+ else
98
+ handle_api_errors
99
+ end
100
+ end
101
+
102
+ def request!(method, *args, &block)
103
+ @response = self.class.client.public_send(method, *args) do |req|
104
+ yield req if block_given?
105
+ end
106
+ if response.success?
107
+ self
108
+ else
109
+ raise_api_exception
110
+ end
111
+ end
112
+
113
+ def raise_api_exception
114
+ case response.status
115
+ when 404
116
+ raise ApiNotFound.new(response), "Record not found"
117
+ when 422
118
+ add_api_errors
119
+ raise RecordInvalidError.new(self, response)
120
+ when 400..499
121
+ raise HttpClientError.new(response), "Client error"
122
+ when 500..599
123
+ raise HttpServerError.new(response), "Server error"
124
+ else
125
+ raise ApiError.new(response), "Api Error"
126
+ end
127
+ end
128
+
129
+ def handle_api_errors
130
+ raise_api_exception
131
+ rescue RecordInvalidError
132
+ false
133
+ end
134
+
135
+ def add_api_errors
136
+ error_message = :unknown_error
137
+
138
+ # Get the custom error message if available, otherwise use the default message
139
+ custom_error_key = "activemodel.errors.models.#{self.class.name.underscore}.response.#{error_message}"
140
+ common_error_key = "activemodel.errors.api_record.response.#{error_message}"
141
+ error_message_text = I18n.t(custom_error_key, default: I18n.t(common_error_key))
142
+
143
+ errors.add(:response, error_message_text)
144
+
145
+ error_response = response.body
146
+ if error_response.is_a?(Hash)
147
+ error_response.each do |key, error_details|
148
+ error_details.each do |message|
149
+ errors.add(key, message)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,28 @@
1
+ require "api_record/base"
2
+ require "api_record/findable"
3
+ module ApiRecord
4
+ module Destroyable
5
+ extend ActiveSupport::Concern
6
+ include ApiRecord::Base
7
+ include ApiRecord::Findable
8
+
9
+ def destroy
10
+ request(:delete, "#{self.class.api_path}/#{id}")
11
+ if response.success?
12
+ true
13
+ else
14
+ handle_api_errors()
15
+ false
16
+ end
17
+ end
18
+
19
+ def destroy!
20
+ request(:delete, "#{self.class.api_path}/#{id}")
21
+ if response.success?
22
+ self
23
+ else
24
+ raise_api_exception()
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ module ApiRecord
2
+ class TimeoutError < StandardError; end
3
+
4
+ class RecordInvalidError < StandardError
5
+ attr_reader :record, :response
6
+
7
+ def initialize(record, response = nil)
8
+ @record = record
9
+ @response = response
10
+ message = "Validation failed: #{record.errors.full_messages.join(', ')}"
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ class ApiInvalidError < StandardError
16
+ attr_reader :response
17
+
18
+ def initialize(response)
19
+ @response = response
20
+ end
21
+ end
22
+
23
+ class ApiError < StandardError
24
+ attr_reader :response
25
+
26
+ def initialize(response)
27
+ @response = response
28
+ end
29
+ end
30
+
31
+ class HttpClientError < ApiError; end
32
+ class HttpServerError < ApiError; end
33
+ class ApiNotFound < HttpClientError; end
34
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'errors'
2
+
3
+ module ApiRecord
4
+ module Findable
5
+ extend ActiveSupport::Concern
6
+
7
+ include Base
8
+
9
+ included do
10
+ attribute :id, :integer
11
+ end
12
+
13
+ class_methods do
14
+ def api_path
15
+ "#{name.pluralize.underscore}"
16
+ end
17
+
18
+ def find(id)
19
+ response = request(:get, "#{api_path}/#{id}")
20
+ if response.success?
21
+ body = response.body.deep_transform_keys!(&:underscore)
22
+ new(body)
23
+ else
24
+ raise_api_exception(response)
25
+ end
26
+ end
27
+ end
28
+
29
+ def new_record?
30
+ id.nil?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module ApiRecord
2
+ IndexCollection = Struct.new(:items, :response)
3
+ end
@@ -0,0 +1,30 @@
1
+ require 'api_record/base'
2
+ require 'api_record/index_collection'
3
+
4
+ module ApiRecord
5
+ module Indexable
6
+ extend ActiveSupport::Concern
7
+ include ApiRecord::Base
8
+
9
+ class_methods do
10
+ def api_path
11
+ "#{name.pluralize.underscore}"
12
+ end
13
+
14
+ def index(page: 1, limit: 30)
15
+ params = {
16
+ page: page,
17
+ limit: limit
18
+ }
19
+ response = request(:get, "#{api_path}", params)
20
+ if response.success?
21
+ ApiRecord::IndexCollection.new(response.body.map do |attributes|
22
+ new(attributes.deep_transform_keys!(&:underscore))
23
+ end, response)
24
+ else
25
+ raise_api_exception(response)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module ApiRecord
2
+ Relation = Struct.new(:data, :response)
3
+ end
@@ -0,0 +1,80 @@
1
+ module ApiRecord
2
+ module Saveable
3
+ extend ActiveSupport::Concern
4
+ include Base
5
+ include Findable
6
+
7
+ class_methods do
8
+ def request_params(param)
9
+ param
10
+ end
11
+
12
+ def create(params)
13
+ record = new(params)
14
+ record.save
15
+ record
16
+ end
17
+
18
+ def create!(params)
19
+ record = new(params)
20
+ record.save!
21
+ record
22
+ end
23
+ end
24
+
25
+ def update!(params)
26
+ self.attributes = params
27
+ save!
28
+ end
29
+
30
+ def update(params)
31
+ self.attributes = params
32
+ save
33
+ end
34
+
35
+ def save
36
+ return false unless valid?
37
+
38
+ result = perform_save_request
39
+ if result
40
+ if response.body.present?
41
+ body = response.body.deep_transform_keys!(&:underscore)
42
+ self.attributes = body
43
+ end
44
+ end
45
+ result
46
+ end
47
+
48
+ def save!
49
+ validate!
50
+
51
+ perform_save_request!
52
+
53
+ if response.body.present?
54
+ body = response.body.deep_transform_keys!(&:underscore)
55
+ self.attributes = body
56
+ end
57
+ self
58
+ end
59
+
60
+ def perform_save_request!
61
+ params = self.class.request_params(attributes)
62
+ params.delete("id")
63
+ if new_record?
64
+ request!(:post, "#{self.class.api_path}", params)
65
+ else
66
+ request!(:put, "#{self.class.api_path}/#{id}", params)
67
+ end
68
+ end
69
+
70
+ def perform_save_request
71
+ params = self.class.request_params(attributes)
72
+ params.delete("id")
73
+ if new_record?
74
+ request(:post, "#{self.class.api_path}", params)
75
+ else
76
+ request(:put, "#{self.class.api_path}/#{id}", params)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiRecord
4
+ VERSION = "0.0.1"
5
+ end
data/lib/api_record.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require_relative "api_record/version"
3
+ require_relative "api_record/all"
4
+
5
+ # module ApiRecord
6
+ # class Error < StandardError; end
7
+ # # Your code goes here...
8
+ # end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Takuya Nishio
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activemodel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '6'
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: '7.2'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '6'
86
+ - - "<"
87
+ - !ruby/object:Gem::Version
88
+ version: '7.2'
89
+ description: Write a longer description or delete this line.
90
+ email:
91
+ - littlecub240@gmail.com
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".rspec"
97
+ - Gemfile
98
+ - Gemfile.lock
99
+ - LICENSE.txt
100
+ - README.md
101
+ - Rakefile
102
+ - bin/console
103
+ - bin/setup
104
+ - lib/api_record.rb
105
+ - lib/api_record/all.rb
106
+ - lib/api_record/base.rb
107
+ - lib/api_record/destroyable.rb
108
+ - lib/api_record/errors.rb
109
+ - lib/api_record/findable.rb
110
+ - lib/api_record/index_collection.rb
111
+ - lib/api_record/indexable.rb
112
+ - lib/api_record/relation.rb
113
+ - lib/api_record/saveable.rb
114
+ - lib/api_record/version.rb
115
+ homepage: https://github.com/webuilder240/api_record
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ homepage_uri: https://github.com/webuilder240/api_record
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 2.6.0
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.2.32
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Write a short summary, because RubyGems requires one.
139
+ test_files: []