adt-driver 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b733d56e30d0ac25c4c388185f2103520fc2e0d6681ffca13507004b9b7e6f3d
4
+ data.tar.gz: 80195deaa7aec3560808ecc4e10e9b90897822da9dce6911b1327f44de52cb43
5
+ SHA512:
6
+ metadata.gz: 0607be88df8a7902d6bc348d5daff6195c3c0a70ab15ab133d7b26fca61350d4266d393189642c24653bb772b0320405ec5a048ce04d30a39c5f85e874a106b3
7
+ data.tar.gz: 57ac5faeb9c95777898bb07463c7e7afea7d5802f830ca2dc5afff88a25fa38007d197019c2d17587d12a2040f15dba5da4e534c760cf4d5601ab86105a29581
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,143 @@
1
+ ---
2
+ Layout/CaseIndentation:
3
+ Enabled: false
4
+ Layout/ElseAlignment:
5
+ Enabled: false
6
+ Layout/EmptyLinesAroundAttributeAccessor:
7
+ Enabled: true
8
+ Layout/EmptyLinesAroundBlockBody:
9
+ Enabled: false
10
+ Layout/EndAlignment:
11
+ Enabled: true
12
+ EnforcedStyleAlignWith: start_of_line
13
+ Layout/FirstArrayElementIndentation:
14
+ EnforcedStyle: consistent
15
+ Layout/FirstHashElementIndentation:
16
+ EnforcedStyle: consistent
17
+ Layout/LineLength:
18
+ Enabled: false
19
+ Layout/MultilineHashBraceLayout:
20
+ Enabled: true
21
+ EnforcedStyle: symmetrical
22
+ Layout/MultilineMethodCallIndentation:
23
+ EnforcedStyle: indented
24
+ Layout/MultilineOperationIndentation:
25
+ EnforcedStyle: indented
26
+ Layout/ParameterAlignment:
27
+ Enabled: true
28
+ EnforcedStyle: with_fixed_indentation
29
+ Layout/SpaceAroundMethodCallOperator:
30
+ Enabled: true
31
+ Layout/SpaceInLambdaLiteral:
32
+ EnforcedStyle: require_space
33
+ Layout/SpaceInsideHashLiteralBraces:
34
+ EnforcedStyle: no_space
35
+ Layout/TrailingWhitespace:
36
+ Enabled: false
37
+ Lint/AmbiguousBlockAssociation:
38
+ Enabled: false
39
+ Lint/AssignmentInCondition:
40
+ Enabled: false
41
+ Lint/DeprecatedOpenSSLConstant:
42
+ Enabled: false
43
+ Lint/MixedRegexpCaptureTypes:
44
+ Enabled: false
45
+ Lint/RaiseException:
46
+ Enabled: false
47
+ Lint/RedundantSplatExpansion:
48
+ Enabled: false
49
+ Lint/StructNewOverride:
50
+ Enabled: false
51
+ Metrics/AbcSize:
52
+ Enabled: false
53
+ Metrics/BlockLength:
54
+ CountComments: false
55
+ Enabled: true
56
+ Exclude:
57
+ - spec/**/*_spec.rb
58
+ Max: 100
59
+ Metrics/ClassLength:
60
+ Enabled: false
61
+ Metrics/CyclomaticComplexity:
62
+ Max: 10
63
+ Metrics/MethodLength:
64
+ Enabled: false
65
+ Metrics/ModuleLength:
66
+ Enabled: false
67
+ Metrics/PerceivedComplexity:
68
+ Max: 10
69
+ Naming/FileName:
70
+ Enabled: false
71
+ Naming/MethodParameterName:
72
+ Enabled: false
73
+ RSpec/AnyInstance:
74
+ Enabled: false
75
+ RSpec/DescribeClass:
76
+ Exclude:
77
+ - spec/requests/**/*_spec.rb
78
+ RSpec/DescribeMethod:
79
+ Enabled: false
80
+ RSpec/DescribedClass:
81
+ Enabled: false
82
+ RSpec/ExampleLength:
83
+ Exclude:
84
+ - spec/workers/**/*_spec.rb
85
+ Max: 25
86
+ RSpec/ImplicitSubject:
87
+ Enabled: false
88
+ RSpec/LetSetup:
89
+ Enabled: false
90
+ RSpec/MessageSpies:
91
+ Enabled: false
92
+ RSpec/MultipleExpectations:
93
+ Enabled: false
94
+ RSpec/NestedGroups:
95
+ Max: 5
96
+ RSpec/NotToNot:
97
+ EnforcedStyle: to_not
98
+ Security/YAMLLoad:
99
+ Exclude:
100
+ - app/models/entity.rb
101
+ Style/Alias:
102
+ Enabled: false
103
+ Style/AsciiComments:
104
+ Enabled: false
105
+ Style/ClassAndModuleChildren:
106
+ Enabled: false
107
+ Style/Documentation:
108
+ Enabled: false
109
+ Style/EmptyMethod:
110
+ EnforcedStyle: expanded
111
+ Style/ExponentialNotation:
112
+ Enabled: true
113
+ Style/FormatStringToken:
114
+ Enabled: false
115
+ Style/FrozenStringLiteralComment:
116
+ Enabled: false
117
+ Style/HashEachMethods:
118
+ Enabled: true
119
+ Style/HashTransformKeys:
120
+ Enabled: true
121
+ Style/HashTransformValues:
122
+ Enabled: true
123
+ Style/Lambda:
124
+ Enabled: false
125
+ Style/NumericPredicate:
126
+ Enabled: false
127
+ Style/PerlBackrefs:
128
+ Enabled: false
129
+ Style/RedundantRegexpCharacterClass:
130
+ Enabled: true
131
+ Style/RedundantRegexpEscape:
132
+ Enabled: true
133
+ Style/RescueModifier:
134
+ Enabled: false
135
+ Style/SafeNavigation:
136
+ Enabled: false
137
+ Style/SlicingWithRange:
138
+ Enabled: true
139
+ Style/SymbolArray:
140
+ EnforcedStyle: brackets
141
+ require:
142
+ - rubocop-rspec
143
+ - rubocop-performance
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ gemspec
2
+ source 'https://rubygems.org'
3
+
4
+ group :development, :test do
5
+ gem 'awesome_print'
6
+ gem 'byebug'
7
+ gem 'guard'
8
+ gem 'guard-rspec'
9
+ gem 'irb'
10
+ gem 'rake'
11
+ gem 'rspec'
12
+ gem 'rubocop'
13
+ gem 'rubocop-performance'
14
+ gem 'rubocop-rspec'
15
+ end
@@ -0,0 +1,107 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ adt-driver (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.1)
10
+ awesome_print (1.8.0)
11
+ byebug (11.1.3)
12
+ coderay (1.1.3)
13
+ diff-lcs (1.3)
14
+ ffi (1.13.1)
15
+ formatador (0.2.5)
16
+ guard (2.16.2)
17
+ formatador (>= 0.2.4)
18
+ listen (>= 2.7, < 4.0)
19
+ lumberjack (>= 1.0.12, < 2.0)
20
+ nenv (~> 0.1)
21
+ notiffany (~> 0.0)
22
+ pry (>= 0.9.12)
23
+ shellany (~> 0.0)
24
+ thor (>= 0.18.1)
25
+ guard-compat (1.2.1)
26
+ guard-rspec (4.7.3)
27
+ guard (~> 2.1)
28
+ guard-compat (~> 1.1)
29
+ rspec (>= 2.99.0, < 4.0)
30
+ io-console (0.5.6)
31
+ irb (1.2.4)
32
+ reline (>= 0.0.1)
33
+ listen (3.2.1)
34
+ rb-fsevent (~> 0.10, >= 0.10.3)
35
+ rb-inotify (~> 0.9, >= 0.9.10)
36
+ lumberjack (1.2.5)
37
+ method_source (1.0.0)
38
+ nenv (0.3.0)
39
+ notiffany (0.1.3)
40
+ nenv (~> 0.1)
41
+ shellany (~> 0.0)
42
+ parallel (1.19.1)
43
+ parser (2.7.1.3)
44
+ ast (~> 2.4.0)
45
+ pry (0.13.1)
46
+ coderay (~> 1.1)
47
+ method_source (~> 1.0)
48
+ rainbow (3.0.0)
49
+ rake (12.3.3)
50
+ rb-fsevent (0.10.4)
51
+ rb-inotify (0.10.1)
52
+ ffi (~> 1.0)
53
+ regexp_parser (1.7.1)
54
+ reline (0.1.4)
55
+ io-console (~> 0.5)
56
+ rexml (3.2.4)
57
+ rspec (3.9.0)
58
+ rspec-core (~> 3.9.0)
59
+ rspec-expectations (~> 3.9.0)
60
+ rspec-mocks (~> 3.9.0)
61
+ rspec-core (3.9.2)
62
+ rspec-support (~> 3.9.3)
63
+ rspec-expectations (3.9.2)
64
+ diff-lcs (>= 1.2.0, < 2.0)
65
+ rspec-support (~> 3.9.0)
66
+ rspec-mocks (3.9.1)
67
+ diff-lcs (>= 1.2.0, < 2.0)
68
+ rspec-support (~> 3.9.0)
69
+ rspec-support (3.9.3)
70
+ rubocop (0.85.1)
71
+ parallel (~> 1.10)
72
+ parser (>= 2.7.0.1)
73
+ rainbow (>= 2.2.2, < 4.0)
74
+ regexp_parser (>= 1.7)
75
+ rexml
76
+ rubocop-ast (>= 0.0.3)
77
+ ruby-progressbar (~> 1.7)
78
+ unicode-display_width (>= 1.4.0, < 2.0)
79
+ rubocop-ast (0.0.3)
80
+ parser (>= 2.7.0.1)
81
+ rubocop-performance (1.6.1)
82
+ rubocop (>= 0.71.0)
83
+ rubocop-rspec (1.40.0)
84
+ rubocop (>= 0.68.1)
85
+ ruby-progressbar (1.10.1)
86
+ shellany (0.0.1)
87
+ thor (1.0.1)
88
+ unicode-display_width (1.7.0)
89
+
90
+ PLATFORMS
91
+ ruby
92
+
93
+ DEPENDENCIES
94
+ adt-driver!
95
+ awesome_print
96
+ byebug
97
+ guard
98
+ guard-rspec
99
+ irb
100
+ rake
101
+ rspec
102
+ rubocop
103
+ rubocop-performance
104
+ rubocop-rspec
105
+
106
+ BUNDLED WITH
107
+ 2.1.4
@@ -0,0 +1,36 @@
1
+ # Adt::Driver
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/adt/driver`. 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 'adt-driver'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install adt-driver
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 tags, 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]/adt-driver.
36
+
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/adt/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'adt-driver'
7
+ spec.version = ADT::VERSION
8
+ spec.authors = ['Sven Clement']
9
+ spec.email = ['clement@clement-weyer.lu']
10
+
11
+ spec.summary = 'ADT is a small Ruby library for reading Advantage Database Server ADT files'
12
+ spec.homepage = 'https://github.com/svnee/adt-driver'
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
14
+
15
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/svnee/adt-driver'
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'adt'
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 'pry'
15
+ Pry.start
@@ -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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'date'
5
+ require 'json'
6
+ require 'forwardable'
7
+ require 'stringio'
8
+
9
+ module ADT
10
+ class Error < StandardError; end
11
+ # Your code goes here...
12
+ end
13
+
14
+ require 'adt/version'
15
+ require 'adt/schema'
16
+ require 'adt/rails'
17
+ require 'adt/table'
18
+ require 'adt/header'
19
+ require 'adt/column_type'
20
+ require 'adt/column'
21
+ require 'adt/record'
@@ -0,0 +1,101 @@
1
+ module ADT
2
+ class Column
3
+ extend Forwardable
4
+
5
+ class LengthError < ADT::Error; end
6
+ class NameError < ADT::Error; end
7
+
8
+ attr_reader :name, :type, :length
9
+
10
+ def_delegator :type_cast_class, :type_cast
11
+ # in use for SAGE BOB50
12
+ # [1, 3, 4, 6, 10, 11, 12, 14, 15, 17]
13
+ TYPE_CAST_CLASS = {
14
+ 1 => ADT::ColumnType::Boolean,
15
+ 3 => ADT::ColumnType::Date,
16
+ 4 => ADT::ColumnType::String,
17
+ 6 => ADT::ColumnType::Binary,
18
+ 10 => ADT::ColumnType::Double,
19
+ 11 => ADT::ColumnType::Integer,
20
+ 12 => ADT::ColumnType::ShortInteger,
21
+ 14 => ADT::ColumnType::DateTime,
22
+ 15 => ADT::ColumnType::Integer, # In reality an "autoinc" but we don't deal with that
23
+ 17 => ADT::ColumnType::CurDouble
24
+ }.freeze
25
+ TYPE_CAST_CLASS.default = ADT::ColumnType::String
26
+ TYPE_CAST_CLASS.freeze
27
+
28
+ # Initialize a new ADT::Column
29
+ #
30
+ # @param [String] name
31
+ # @param [String] type
32
+ # @param [Integer] length
33
+ def initialize(name, type, length)
34
+ @name = clean(name)
35
+ @type = type
36
+ @length = length
37
+
38
+ validate_length
39
+ validate_name
40
+ end
41
+
42
+ # Return the flag to decode the data
43
+ #
44
+ # @param [Integer] length
45
+ def flag(length = 0)
46
+ flag = type_cast_class.flag
47
+ return flag + length.to_s if flag.eql? 'A'
48
+
49
+ flag
50
+ end
51
+
52
+ # Returns a Hash with :name, :type, :length, and :decimal keys
53
+ #
54
+ # @return [Hash]
55
+ def to_hash
56
+ {name: name, type: type, length: length}
57
+ end
58
+
59
+ def downcase
60
+ name.downcase
61
+ end
62
+
63
+ # Underscored name
64
+ #
65
+ # This is the column name converted to underscore format.
66
+ # For example, MyColumn will be returned as my_column.
67
+ #
68
+ # @return [String]
69
+ def underscored_name
70
+ @underscored_name ||= begin
71
+ name.gsub(/::/, '/')
72
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
73
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
74
+ .tr('-', '_')
75
+ .downcase
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def clean(value) # :nodoc:
82
+ truncated_value = value.strip.partition("\x00").first
83
+ truncated_value.gsub(/[^\x20-\x7E]/, '')
84
+ end
85
+
86
+ def validate_length # :nodoc:
87
+ raise LengthError, 'field length must be 0 or greater' if length < 0
88
+ end
89
+
90
+ def validate_name # :nodoc:
91
+ raise NameError, 'column name cannot be empty' if @name.empty?
92
+ end
93
+
94
+ def type_cast_class # :nodoc:
95
+ @type_cast_class ||= begin
96
+ klass = @length == 0 ? ColumnType::Nil : TYPE_CAST_CLASS[type]
97
+ klass.new
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,130 @@
1
+ module ADT
2
+ module ColumnType
3
+ class MemoryError < ADT::Error; end
4
+
5
+ class Base; end
6
+
7
+ class Nil < Base
8
+ def type_cast(_value, _table)
9
+ nil
10
+ end
11
+
12
+ def flag
13
+ ''
14
+ end
15
+ end
16
+
17
+ class Boolean < Base
18
+ def type_cast(value, _table)
19
+ value.strip.match?(/^(y|t)$/i)
20
+ end
21
+
22
+ def flag
23
+ ''
24
+ end
25
+ end
26
+
27
+ class Binary < Base
28
+ def type_cast(value, table)
29
+ raise MemoryError, '.adm file is missing' unless table.memory?
30
+
31
+ offset, length = value.unpack('I*')
32
+ table.memory.seek(offset * 8)
33
+ table.memory.read(length)
34
+ end
35
+
36
+ def flag
37
+ ''
38
+ end
39
+ end
40
+
41
+ class Double < Base
42
+ def type_cast(value, _table)
43
+ value.unpack1('D') <= 0.001 ? 0.0 : value.unpack1('D')
44
+ end
45
+
46
+ def flag
47
+ 'D'
48
+ end
49
+ end
50
+
51
+ class Integer < Base
52
+ def type_cast(value, _table)
53
+ return nil if value.strip.empty?
54
+
55
+ value.unpack('L').dig(0).to_i == 2_147_483_648 ? nil : value.unpack('L').dig(0).to_i
56
+ end
57
+
58
+ def flag
59
+ 'i'
60
+ end
61
+ end
62
+
63
+ class ShortInteger < Base
64
+ def type_cast(value, _table)
65
+ return nil if value.strip.empty?
66
+
67
+ value.unpack('S_').dig(0).to_i == 2_147_483_648 ? nil : value.unpack('S_').dig(0).to_i
68
+ end
69
+
70
+ def flag
71
+ 'i'
72
+ end
73
+ end
74
+
75
+ class Date < Base
76
+ def type_cast(value, _table)
77
+ int = value.unpack('L').dig(0).to_i
78
+ int == 0 ? nil : ::Date.jd(int)
79
+ rescue StandardError
80
+ nil
81
+ end
82
+
83
+ def flag
84
+ '?'
85
+ end
86
+ end
87
+
88
+ class DateTime < Base
89
+ def type_cast(value, _table)
90
+ val = value.unpack('L L')
91
+ date = ::Date.jd(val.dig(0))
92
+ ::Time.at(date.to_time + val.dig(1) / 1000).to_datetime
93
+ end
94
+
95
+ def flag
96
+ '?'
97
+ end
98
+ end
99
+
100
+ class CurDouble < Base
101
+ def type_cast(value, _table)
102
+ value.unpack('D').dig(0) < 0.001 ? nil : value.unpack('D').dig(0)
103
+ end
104
+
105
+ def flag
106
+ 'D'
107
+ end
108
+ end
109
+
110
+ class String < Base
111
+ def type_cast(value, _table)
112
+ value = value.strip
113
+ value.force_encoding('UTF-8').encode('UTF-8', undef: :replace, invalid: :replace)
114
+ end
115
+
116
+ def flag
117
+ 'A'
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # TYPES = {17 => 'curdouble', 4 => 'character', 10 => 'double', 11 => 'integer', 12 => 'short', 20 => 'cicharacter', 3 => 'date', 13 => 'time', 14 => 'timestamp', 15 => 'autoinc'}
124
+ # FLAGS = {'character' => 'A', 'double' => 'D', 'integer' => 'i', 'short' => 'S', 'cicharacter' => 'A', 'date' => '?', 'time' => '?', 'timestamp' => '?', 'autoinc' => 'I'}
125
+ # in use for SAGE BOB50
126
+ # [1, 3, 4, 6, 10, 11, 12, 14, 15, 17]
127
+ # 6 => Binary
128
+ # 10 => Double
129
+ # 12 => Shortint
130
+ # 15 => autoinc
@@ -0,0 +1,18 @@
1
+ module ADT
2
+ class Header
3
+ attr_reader :record_count
4
+ attr_reader :data_offset
5
+ attr_reader :record_length
6
+ attr_reader :column_count
7
+
8
+ def initialize(data)
9
+ @data = data
10
+ @record_count, @data_offset, @record_length = unpack_header
11
+ @column_count = (@data_offset - 400) / 200
12
+ end
13
+
14
+ def unpack_header
15
+ @data.unpack('@24 I x4 I I')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module ADT
2
+ # The Model module is mixin for the Table class
3
+ # It provides generators for imported tables
4
+ module Rails
5
+ def model
6
+ "class #{camelize(name)} < ApplicationRecord
7
+ self.table_name = '#{name}'
8
+ end"
9
+ end
10
+
11
+ private
12
+
13
+ def camelize(string, uppercase_first_letter = true)
14
+ string = if uppercase_first_letter
15
+ string.sub(/^[a-z\d]*/, &:capitalize)
16
+ else
17
+ string.sub(/^(?:(?=\b|[A-Z_])|\w)/, &:downcase)
18
+ end
19
+ string.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,88 @@
1
+ module ADT
2
+ # An instance of ADT::Record represents a row in the ADT file
3
+ class Record
4
+ # Initialize a new ADT::Record
5
+ #
6
+ # @data [String, StringIO] data
7
+ # @columns [Column]
8
+ def initialize(data, columns, table)
9
+ @data = ::StringIO.new(data)
10
+ @data.seek(4) # We don't know what the first 4 bytes are
11
+ @table = table
12
+ @columns = columns
13
+ end
14
+
15
+ # Record attributes
16
+ #
17
+ # @return [Hash]
18
+ def attributes
19
+ @attributes ||= Hash[attribute_map]
20
+ end
21
+
22
+ # Maps a row to an array of values
23
+ #
24
+ # @return [Array]
25
+ def to_a
26
+ @columns.map { |column| attributes[column.name] }
27
+ end
28
+
29
+ # Do all search parameters match?
30
+ #
31
+ # @param [Hash] options
32
+ # @return [Boolean]
33
+ def match?(options)
34
+ options.all? { |key, value| self[key] == value }
35
+ end
36
+
37
+ # Reads attributes by column name
38
+ #
39
+ # @param [String, Symbol] key
40
+ def [](name)
41
+ key = name.to_s
42
+ if attributes.key?(key)
43
+ attributes[key]
44
+ elsif index = underscored_column_names.index(key)
45
+ attributes[@columns[index].name]
46
+ end
47
+ end
48
+
49
+ # Equality
50
+ #
51
+ # @param [ADT::Record] other
52
+ # @return [Boolean]
53
+ def ==(other)
54
+ other.respond_to?(:attributes) && other.attributes == attributes
55
+ end
56
+
57
+ private
58
+
59
+ def attribute_map # :nodoc:
60
+ @columns.map { |column| [column.name, init_attribute(column)] }
61
+ end
62
+
63
+ def get_data(column) # :nodoc:
64
+ @data.read(column.length)
65
+ end
66
+
67
+ def init_attribute(column) # :nodoc:
68
+ value = get_data(column)
69
+ column.type_cast(value, @table)
70
+ end
71
+
72
+ def method_missing(method, *args) # :nodoc:
73
+ if (index = underscored_column_names.index(method.to_s))
74
+ attributes[@columns[index].name]
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ def respond_to_missing?(method, *) # :nodoc:
81
+ underscored_column_names.include?(method.to_s) || super
82
+ end
83
+
84
+ def underscored_column_names # :nodoc:
85
+ @underscored_column_names ||= @columns.map(&:underscored_name)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,87 @@
1
+ module ADT
2
+ # The Schema module is mixin for the Table class
3
+ module Schema
4
+ FORMATS = [:activerecord, :json].freeze
5
+
6
+ OTHER_DATA_TYPES = {
7
+ 1 => ':boolean',
8
+ 3 => ':date',
9
+ 6 => ':text',
10
+ 10 => ':decimal, precision: 6',
11
+ 11 => ':integer',
12
+ 12 => ':integer',
13
+ 14 => ':datetime',
14
+ 15 => ':integer', # In reality an "autoinc" but we don't deal with that
15
+ 17 => ':decimal, precision: 6'
16
+ }.freeze
17
+
18
+ # Generate an ActiveRecord::Schema
19
+ #
20
+ # ADT data types are converted to generic types as follows:
21
+ # - Number columns with no decimals are converted to :integer
22
+ # - Number columns with decimals are converted to :decimal
23
+ # - Date columns are converted to :date
24
+ # - DateTime columns are converted to :datetime
25
+ # - Logical columns are converted to :boolean
26
+ # - Binary columns are converted to :text
27
+ # - Character columns are converted to :string and the :limit option is set
28
+ # to the length of the character column
29
+ #
30
+ # Example:
31
+ # create_table "mydata" do |t|
32
+ # t.column :name, :string, :limit => 30
33
+ # t.column :last_update, :datetime
34
+ # t.column :is_active, :boolean
35
+ # t.column :age, :integer
36
+ # t.column :notes, :text
37
+ # end
38
+ #
39
+ # @param [Symbol] format Valid options are :activerecord and :json
40
+ # @return [String]
41
+ def schema(format = :activerecord, table_only = false)
42
+ schema_method_name = schema_name(format)
43
+ send(schema_method_name, table_only)
44
+ rescue NameError
45
+ raise ArgumentError, ":#{format} is not a valid schema. Valid schemas are: #{FORMATS.join(', ')}."
46
+ end
47
+
48
+ def schema_name(format) # :nodoc:
49
+ "#{format}_schema"
50
+ end
51
+
52
+ def activerecord_schema(_table_only = false) # :nodoc:
53
+ s = "ActiveRecord::Schema.define do\n"
54
+ s << " create_table \"#{name}\" #{'{id: false}' if columns.map(&:downcase).include?('id')} do |t|\n"
55
+ columns.each do |column|
56
+ s << " t.column #{activerecord_schema_definition(column)}"
57
+ end
58
+ s << " end\nend"
59
+ s
60
+ end
61
+
62
+ def json_schema(_table_only = false) # :nodoc:
63
+ columns.map(&:to_hash).to_json
64
+ end
65
+
66
+ # ActiveRecord schema definition
67
+ #
68
+ # @param [DBF::Column]
69
+ # @return [String]
70
+ def activerecord_schema_definition(column)
71
+ "\"#{column.underscored_name}\", #{schema_data_type(column)}\n"
72
+ end
73
+
74
+ def schema_data_type(column) # :nodoc:
75
+ case column.type
76
+ when *%w[1 3 6 10 11 12 14 15 17]
77
+ OTHER_DATA_TYPES[column.type]
78
+ else
79
+ string_data_format(column)
80
+ end
81
+ end
82
+
83
+ def string_data_format(column)
84
+ ":string, :limit => #{column.length}"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,142 @@
1
+ module ADT
2
+ class FileNotFoundError < StandardError
3
+ end
4
+
5
+ # ADT::Table is the primary interface to a single ADT file and provides
6
+ # methods for enumerating and searching the records.
7
+ class Table
8
+ extend Forwardable
9
+ include ::ADT::Schema
10
+ include ::ADT::Rails
11
+
12
+ ADT_HEADER_SIZE = 400
13
+
14
+ def_delegator :header, :record_count
15
+ def_delegator :header, :data_offset
16
+ def_delegator :header, :record_length
17
+ def_delegator :header, :column_count
18
+
19
+ attr_reader :memory
20
+
21
+ # Opens a ADT::Table
22
+ # Examples:
23
+ # # working with a file stored on the filesystem
24
+ # table = ADT::Table.new 'data.adt'
25
+ #
26
+ # @param [String] data Path to the adt file
27
+ def initialize(data)
28
+ @data = open_data(data)
29
+ @memory = open_data(data.gsub('.adt', '.adm')) if File.exist?(data.gsub('.adt', '.adm'))
30
+ yield self if block_given?
31
+ end
32
+
33
+ # Closes the table
34
+ #
35
+ # @return [TrueClass, FalseClass]
36
+ def close
37
+ @data.close
38
+ end
39
+
40
+ # @return [TrueClass, FalseClass]
41
+ def memory?
42
+ !@memory.nil?
43
+ end
44
+
45
+ # @return [TrueClass, FalseClass]
46
+ def closed?
47
+ @data.closed?
48
+ end
49
+
50
+ # Retrieve a record by index number.
51
+ # The record will be nil if it has been deleted, but not yet pruned from
52
+ # the database.
53
+ #
54
+ # @param [Integer] index
55
+ # @return [ADT::Record, NilClass]
56
+ def record(index)
57
+ seek_to_record(index)
58
+ return nil if deleted_record?
59
+
60
+ ADT::Record.new(@data.read(record_length), columns, self)
61
+ end
62
+
63
+ # All columns
64
+ #
65
+ # @return [Array]
66
+ def columns
67
+ @columns ||= build_columns
68
+ end
69
+
70
+ # Column names
71
+ #
72
+ # @return [String]
73
+ def column_names
74
+ columns.map(&:name)
75
+ end
76
+
77
+ # @return [String]
78
+ def filename
79
+ return unless @data.respond_to?(:path)
80
+
81
+ File.basename(@data.path)
82
+ end
83
+
84
+ attr_reader :data
85
+
86
+ def name
87
+ filename.gsub('.adt', '')
88
+ end
89
+
90
+ private
91
+
92
+ def build_columns # :nodoc:
93
+ safe_seek do
94
+ # skip past header to get to column information
95
+ @data.seek(ADT_HEADER_SIZE)
96
+ # column names are the first 128 bytes and column info takes up the last 72 bytes.
97
+ # byte 130 contains a 16-bit column type
98
+ # byte 136 contains a 16-bit length field
99
+ @columns = []
100
+ column_count.times do
101
+ name, type, length = @data.read(200).unpack('A128 x S x4 S')
102
+ @columns << ADT::Column.new(name.strip, type, length) if length > 0
103
+ end
104
+ # Reset the column count in case any were skipped
105
+ @column_count = @columns.size
106
+
107
+ @columns
108
+ end
109
+ end
110
+
111
+ def deleted_record? # :nodoc:
112
+ flag = @data.read(1)
113
+ flag ? flag.unpack1('a') == '*' : true
114
+ end
115
+
116
+ def header # :nodoc:
117
+ @header ||= safe_seek do
118
+ @data.seek(0)
119
+ Header.new(@data.read(ADT_HEADER_SIZE))
120
+ end
121
+ end
122
+
123
+ def open_data(data) # :nodoc:
124
+ File.open(data, 'rb')
125
+ rescue Errno::ENOENT
126
+ raise ADT::FileNotFoundError, "file not found: #{data}"
127
+ end
128
+
129
+ def safe_seek # :nodoc:
130
+ original_pos = @data.pos
131
+ yield.tap { @data.seek(original_pos) }
132
+ end
133
+
134
+ def seek(offset) # :nodoc:
135
+ @data.seek(data_offset + offset)
136
+ end
137
+
138
+ def seek_to_record(index) # :nodoc:
139
+ seek(index * record_length)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ADT
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adt-driver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sven Clement
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-06-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - clement@clement-weyer.lu
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - ".travis.yml"
24
+ - Gemfile
25
+ - Gemfile.lock
26
+ - README.md
27
+ - Rakefile
28
+ - adt-driver.gemspec
29
+ - bin/console
30
+ - bin/setup
31
+ - lib/adt.rb
32
+ - lib/adt/column.rb
33
+ - lib/adt/column_type.rb
34
+ - lib/adt/header.rb
35
+ - lib/adt/rails.rb
36
+ - lib/adt/record.rb
37
+ - lib/adt/schema.rb
38
+ - lib/adt/table.rb
39
+ - lib/adt/version.rb
40
+ homepage: https://github.com/svnee/adt-driver
41
+ licenses: []
42
+ metadata:
43
+ homepage_uri: https://github.com/svnee/adt-driver
44
+ source_code_uri: https://github.com/svnee/adt-driver
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.3.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.1.2
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: ADT is a small Ruby library for reading Advantage Database Server ADT files
64
+ test_files: []