├── spec ├── spec_helper.rb └── parser_spec.rb ├── lib ├── pg_array_parser │ └── version.rb ├── pg_array_parser.jar └── pg_array_parser.rb ├── ext └── pg_array_parser │ ├── extconf.rb │ ├── PgArrayParserEngineService.java │ ├── pg_array_parser.c │ └── PgArrayParserEngine.java ├── Gemfile ├── .travis.yml ├── .gitignore ├── Rakefile ├── CHANGELOG.md ├── README.md └── pg_array_parser.gemspec /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pg_array_parser' 2 | -------------------------------------------------------------------------------- /lib/pg_array_parser/version.rb: -------------------------------------------------------------------------------- 1 | module PgArrayParser 2 | VERSION = '0.0.9' 3 | end 4 | -------------------------------------------------------------------------------- /ext/pg_array_parser/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | create_makefile('pg_array_parser/pg_array_parser') 3 | -------------------------------------------------------------------------------- /lib/pg_array_parser.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavyJonesLocker/pg_array_parser/HEAD/lib/pg_array_parser.jar -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in pg_array_parser.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - jruby-18mode 5 | - jruby-19mode 6 | 7 | notifications: 8 | email: 9 | - travis@danmcclain.net 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | benchmark.rb 19 | bin/* 20 | .rbenv-version* 21 | lib/pg_array_parser.bundle 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'bundler/gem_tasks' 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | spec = Gem::Specification.load('pg_array_parser.gemspec') 7 | 8 | if RUBY_PLATFORM =~ /java/ 9 | require 'rake/javaextensiontask' 10 | Rake::JavaExtensionTask.new('pg_array_parser', spec) 11 | else 12 | require 'rake/extensiontask' 13 | Rake::ExtensionTask.new('pg_array_parser', spec) 14 | end 15 | 16 | task :install => :compile 17 | task :spec => :install 18 | 19 | RSpec::Core::RakeTask.new(:spec) 20 | 21 | task :default => :spec 22 | -------------------------------------------------------------------------------- /lib/pg_array_parser.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../pg_array_parser/version', __FILE__) 2 | 3 | if RUBY_PLATFORM =~ /java/ 4 | module PgArrayParser 5 | require 'jruby' 6 | require File.expand_path('../pg_array_parser.jar', __FILE__) 7 | require 'pgArrayParser/pg_array_parser_engine' 8 | 9 | def parse_pg_array(value) 10 | @parser ||= PgArrayParserEngine.new 11 | @parser.parse_pg_array(value) 12 | end 13 | end 14 | else 15 | begin 16 | require 'pg_array_parser/pg_array_parser' 17 | rescue LoadError 18 | begin 19 | require "pg_array_parser/pg_array_parser.#{RbConfig::CONFIG['DLEXT']}" 20 | rescue LoadError 21 | require "pg_array_parser.#{RbConfig::CONFIG['DLEXT']}" 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.7 2 | * Trying to not break the C extension now 3 | 4 | # 0.0.6 5 | * Excludes C extension files in JRuby 6 | 7 | # 0.0.5 8 | * Prevents attempt to build C extension in JRuby 9 | 10 | # 0.0.4 11 | * Fixes bad release 12 | 13 | # 0.0.3 14 | * Refactored Java to ensure thread safety - [mauricio](https://github.com/mauricio) - 15 | Merged at [a30aba](https://github.com/dockyard/pg_array_parser/commit/a30aba4885812290f83c693e6b68c697b0dac675) 16 | 17 | # 0.0.2 18 | * Refactored C extension - [jeremyevans](https://github.com/jeremyevens) - Merged at [ad4987](https://github.com/dockyard/pg_array_parser/commit/ad4987dba411decca4aebd0750c990212dc81039) 19 | * Adds JRuby support - thanks to [tychobrailleur](https://github.com/tychobrailleur) for help with the java class 20 | 21 | # 0.0.1 22 | * Initial release 23 | -------------------------------------------------------------------------------- /ext/pg_array_parser/PgArrayParserEngineService.java: -------------------------------------------------------------------------------- 1 | package pgarrayparser; 2 | 3 | import org.jruby.Ruby; 4 | import org.jruby.RubyClass; 5 | import org.jruby.RubyModule; 6 | import org.jruby.runtime.ObjectAllocator; 7 | import org.jruby.runtime.builtin.IRubyObject; 8 | import org.jruby.runtime.load.BasicLibraryService; 9 | 10 | import java.io.IOException; 11 | 12 | public class PgArrayParserEngineService implements BasicLibraryService { 13 | 14 | public boolean basicLoad(Ruby runtime) throws IOException { 15 | 16 | RubyModule pgArrayParser = runtime.defineModule("PgArrayParser"); 17 | RubyClass pgArrayParserEngine = pgArrayParser.defineClassUnder("PgArrayParserEngine", runtime.getObject(), new ObjectAllocator() { 18 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 19 | return new PgArrayParserEngine(runtime, rubyClass); 20 | } 21 | }); 22 | 23 | pgArrayParserEngine.defineAnnotatedMethods(PgArrayParserEngine.class); 24 | return true; 25 | } 26 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PgArrayParser 2 | [![Build Status](http://travis-ci.org/dockyard/easy_auth.png)](http://travis-ci.org/dockyard/pg_array_parser) 3 | [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/dockyard/pg_array_parser) 4 | 5 | Fast PostreSQL array parsing. 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | ```ruby 11 | gem 'pg_array_parser' 12 | ``` 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install pg_array_parser 21 | 22 | ## Usage 23 | 24 | Include the `PgArrayParser` module, which provides the `parse_pg_array` 25 | method. 26 | 27 | ```ruby 28 | class MyPostgresParser 29 | include PgArrayParser 30 | end 31 | 32 | parser = MyPostgresParser.new 33 | parser.parse_pg_array '{}' 34 | # => [] 35 | parser.parse_pg_array '{1,2,3,4}' 36 | # => ["1", "2", "3", "4"] 37 | parser.parse_pg_array '{1,{2,3},4}' 38 | # => ["1", ["2", "3"], "4"] 39 | parser.parse_pg_array '{some,strings that,"May have some ,\'s"}' 40 | # => ["some", "strings that", "May have some ,'s"] 41 | ``` 42 | 43 | ## Authors 44 | 45 | [Dan McClain](http://github.com/danmcclain) [twitter](http://twitter.com/_danmcclain) 46 | 47 | ## Versioning ## 48 | 49 | This gem follows [Semantic Versioning](http://semver.org) 50 | 51 | ## Want to help? ## 52 | 53 | Stable branches are created based upon each minor version. Please make 54 | pull requests to specific branches rather than master. 55 | 56 | Please make sure you include tests! 57 | 58 | Don't use tabs to indent, two spaces are the standard. 59 | 60 | ## Legal ## 61 | 62 | [DockYard](http://dockyard.com), LLC © 2012 63 | 64 | [@dockyard](http://twitter.com/dockyard) 65 | 66 | [Licensed under the MIT 67 | license](http://www.opensource.org/licenses/mit-license.php) 68 | -------------------------------------------------------------------------------- /pg_array_parser.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/pg_array_parser/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Dan McClain"] 6 | gem.email = ["git@danmcclain.net"] 7 | gem.description = %q{Simple library to parse PostgreSQL arrays into a array of strings} 8 | gem.summary = %q{Converts PostgreSQL array strings into arrays of strings} 9 | gem.homepage = "https://github.com/dockyard/pg_array_parser" 10 | 11 | gem.files = [ 'CHANGELOG.md', 12 | 'Gemfile', 13 | 'README.md', 14 | 'Rakefile', 15 | 'lib/pg_array_parser.rb', 16 | 'lib/pg_array_parser/version.rb', 17 | 'pg_array_parser.gemspec', 18 | 'spec/parser_spec.rb', 19 | 'spec/spec_helper.rb' 20 | ] 21 | 22 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 23 | if RUBY_PLATFORM =~ /java/ 24 | gem.platform = 'java' 25 | gem.files << 'ext/pg_array_parser/PgArrayParserEngine.java' 26 | gem.files << 'ext/pg_array_parser/PgArrayParserEngineService.java' 27 | gem.files << 'lib/pg_array_parser.jar' 28 | else 29 | gem.files << 'ext/pg_array_parser/pg_array_parser.c' 30 | gem.extensions = ['ext/pg_array_parser/extconf.rb'] 31 | end 32 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 33 | gem.name = "pg_array_parser" 34 | gem.require_paths = ["lib"] 35 | gem.version = PgArrayParser::VERSION 36 | 37 | gem.add_development_dependency 'rspec', '~> 2.11.0' 38 | gem.add_development_dependency 'rake', '~> 0.9.2.2' 39 | gem.add_development_dependency 'rake-compiler' 40 | end 41 | -------------------------------------------------------------------------------- /ext/pg_array_parser/pg_array_parser.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* Prototype */ 5 | VALUE read_array(int *index, char *string, int length, char *word, rb_encoding *enc); 6 | 7 | VALUE parse_pg_array(VALUE self, VALUE pg_array_string) { 8 | 9 | /* convert to c-string, create a buffer of the same length, as that will be the worst case */ 10 | char *c_pg_array_string = StringValueCStr(pg_array_string); 11 | int array_string_length = RSTRING_LEN(pg_array_string); 12 | char *word = malloc(array_string_length + 1); 13 | rb_encoding *enc = rb_enc_get(pg_array_string); 14 | 15 | int index = 1; 16 | 17 | VALUE return_value = read_array(&index, c_pg_array_string, array_string_length, word, enc); 18 | free(word); 19 | return return_value; 20 | } 21 | 22 | VALUE read_array(int *index, char *c_pg_array_string, int array_string_length, char *word, rb_encoding *enc) 23 | { 24 | /* Return value: array */ 25 | VALUE array; 26 | int word_index = 0; 27 | 28 | /* The current character in the input string. */ 29 | char c; 30 | 31 | /* 0: Currently outside a quoted string, current word never quoted 32 | * 1: Currently inside a quoted string 33 | * -1: Currently outside a quoted string, current word previously quoted */ 34 | int openQuote = 0; 35 | 36 | /* Inside quoted input means the next character should be treated literally, 37 | * instead of being treated as a metacharacter. 38 | * Outside of quoted input, means that the word shouldn't be pushed to the array, 39 | * used when the last entry was a subarray (which adds to the array itself). */ 40 | int escapeNext = 0; 41 | 42 | array = rb_ary_new(); 43 | 44 | /* Special case the empty array, so it doesn't need to be handled manually inside 45 | * the loop. */ 46 | if(((*index) < array_string_length) && c_pg_array_string[(*index)] == '}') 47 | { 48 | return array; 49 | } 50 | 51 | for(;(*index) < array_string_length; ++(*index)) 52 | { 53 | c = c_pg_array_string[*index]; 54 | if(openQuote < 1) 55 | { 56 | if(c == ',' || c == '}') 57 | { 58 | if(!escapeNext) 59 | { 60 | if(openQuote == 0 && word_index == 4 && !strncmp(word, "NULL", word_index)) 61 | { 62 | rb_ary_push(array, Qnil); 63 | } 64 | else 65 | { 66 | rb_ary_push(array, rb_enc_str_new(word, word_index, enc)); 67 | } 68 | } 69 | if(c == '}') 70 | { 71 | return array; 72 | } 73 | escapeNext = 0; 74 | openQuote = 0; 75 | word_index = 0; 76 | } 77 | else if(c == '"') 78 | { 79 | openQuote = 1; 80 | } 81 | else if(c == '{') 82 | { 83 | (*index)++; 84 | rb_ary_push(array, read_array(index, c_pg_array_string, array_string_length, word, enc)); 85 | escapeNext = 1; 86 | } 87 | else 88 | { 89 | word[word_index] = c; 90 | word_index++; 91 | } 92 | } 93 | else if (escapeNext) { 94 | word[word_index] = c; 95 | word_index++; 96 | escapeNext = 0; 97 | } 98 | else if (c == '\\') 99 | { 100 | escapeNext = 1; 101 | } 102 | else if (c == '"') 103 | { 104 | openQuote = -1; 105 | } 106 | else 107 | { 108 | word[word_index] = c; 109 | word_index++; 110 | } 111 | } 112 | 113 | return array; 114 | } 115 | 116 | void Init_pg_array_parser(void) { 117 | rb_define_method(rb_define_module("PgArrayParser"), "parse_pg_array", parse_pg_array, 1); 118 | } 119 | 120 | -------------------------------------------------------------------------------- /spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | class Parser 5 | include PgArrayParser 6 | end 7 | 8 | describe 'PgArrayParser' do 9 | let!(:parser) { Parser.new } 10 | 11 | describe '#parse_pg_array' do 12 | context 'one dimensional arrays' do 13 | context 'empty' do 14 | it 'returns an empty array' do 15 | parser.parse_pg_array(%[{}]).should eq [] 16 | end 17 | end 18 | 19 | context 'no strings' do 20 | it 'returns an array of strings' do 21 | parser.parse_pg_array(%[{1,2,3}]).should eq ['1','2','3'] 22 | end 23 | end 24 | 25 | context 'NULL values' do 26 | it 'returns an array of strings, with nils replacing NULL characters' do 27 | parser.parse_pg_array(%[{1,NULL,NULL}]).should eq ['1',nil,nil] 28 | end 29 | end 30 | 31 | context 'quoted NULL' do 32 | it 'returns an array with the word NULL' do 33 | parser.parse_pg_array(%[{1,"NULL",3}]).should eq ['1','NULL','3'] 34 | end 35 | end 36 | 37 | context 'strings' do 38 | it 'returns an array of strings when containing commas in a quoted string' do 39 | parser.parse_pg_array(%[{1,"2,3",4}]).should eq ['1','2,3','4'] 40 | end 41 | 42 | it 'returns an array of strings when containing an escaped quote' do 43 | parser.parse_pg_array(%[{1,"2\\",3",4}]).should eq ['1','2",3','4'] 44 | end 45 | 46 | it 'returns an array of strings when containing an escaped backslash' do 47 | parser.parse_pg_array(%[{1,"2\\\\",3,4}]).should eq ['1','2\\','3','4'] 48 | parser.parse_pg_array(%[{1,"2\\\\\\",3",4}]).should eq ['1','2\\",3','4'] 49 | end 50 | 51 | it 'returns an array containing empty strings' do 52 | parser.parse_pg_array(%[{1,"",3,""}]).should eq ['1', '', '3', ''] 53 | end 54 | 55 | it 'returns an array containing unicode strings' do 56 | parser.parse_pg_array(%[{"Paragraph 399(b)(i) – “valid leave” – meaning"}]).should eq(['Paragraph 399(b)(i) – “valid leave” – meaning']) 57 | end 58 | end 59 | end 60 | 61 | context 'two dimensional arrays' do 62 | context 'empty' do 63 | it 'returns an empty array' do 64 | parser.parse_pg_array(%[{{}}]).should eq [[]] 65 | parser.parse_pg_array(%[{{},{}}]).should eq [[],[]] 66 | end 67 | end 68 | context 'no strings' do 69 | it 'returns an array of strings with a sub array' do 70 | parser.parse_pg_array(%[{1,{2,3},4}]).should eq ['1',['2','3'],'4'] 71 | end 72 | end 73 | context 'strings' do 74 | it 'returns an array of strings with a sub array' do 75 | parser.parse_pg_array(%[{1,{"2,3"},4}]).should eq ['1',['2,3'],'4'] 76 | end 77 | it 'returns an array of strings with a sub array and a quoted }' do 78 | parser.parse_pg_array(%[{1,{"2,}3",NULL},4}]).should eq ['1',['2,}3',nil],'4'] 79 | end 80 | it 'returns an array of strings with a sub array and a quoted {' do 81 | parser.parse_pg_array(%[{1,{"2,{3"},4}]).should eq ['1',['2,{3'],'4'] 82 | end 83 | it 'returns an array of strings with a sub array and a quoted { and escaped quote' do 84 | parser.parse_pg_array(%[{1,{"2\\",{3"},4}]).should eq ['1',['2",{3'],'4'] 85 | end 86 | it 'returns an array of strings with a sub array with empty strings' do 87 | parser.parse_pg_array(%[{1,{""},4,{""}}]).should eq ['1',[''],'4',['']] 88 | end 89 | end 90 | end 91 | context 'three dimensional arrays' do 92 | context 'empty' do 93 | it 'returns an empty array' do 94 | parser.parse_pg_array(%[{{{}}}]).should eq [[[]]] 95 | parser.parse_pg_array(%[{{{},{}},{{},{}}}]).should eq [[[],[]],[[],[]]] 96 | end 97 | end 98 | it 'returns an array of strings with sub arrays' do 99 | parser.parse_pg_array(%[{1,{2,{3,4}},{NULL,6},7}]).should eq ['1',['2',['3','4']],[nil,'6'],'7'] 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /ext/pg_array_parser/PgArrayParserEngine.java: -------------------------------------------------------------------------------- 1 | package pgarrayparser; 2 | 3 | import org.jruby.*; 4 | import org.jruby.anno.JRubyClass; 5 | import org.jruby.anno.JRubyMethod; 6 | import org.jruby.runtime.ThreadContext; 7 | import org.jruby.runtime.builtin.IRubyObject; 8 | 9 | @JRubyClass(name = "PgArrayParser::PgArrayParserEngine") 10 | public class PgArrayParserEngine extends RubyObject { 11 | 12 | public PgArrayParserEngine(final Ruby runtime, RubyClass rubyClass) { 13 | super(runtime, rubyClass); 14 | } 15 | 16 | @JRubyMethod(name = "parse_pg_array") 17 | public RubyArray parse_pg_array( ThreadContext context, IRubyObject value) { 18 | String content = value.asJavaString(); 19 | return parseData(context, content, 0); 20 | } 21 | 22 | private static RubyArray parseData( ThreadContext context, String content, int index) 23 | { 24 | RubyArray items = RubyArray.newArray(context.getRuntime()); 25 | 26 | for( int x = index; x < content.length(); x++ ) { 27 | 28 | char token = content.charAt(x); 29 | 30 | switch (token) { 31 | case '{': 32 | x = parseArrayContents( context, items, content, x + 1 ); 33 | break; 34 | case '}': 35 | return items; 36 | } 37 | 38 | } 39 | 40 | return items; 41 | } 42 | 43 | private static int parseArrayContents( ThreadContext context, RubyArray items, String content, int index ) { 44 | 45 | StringBuilder currentItem = new StringBuilder(); 46 | boolean isEscaping = false; 47 | boolean isQuoted = false; 48 | boolean wasQuoted = false; 49 | 50 | int x = index; 51 | 52 | for(; x < content.length(); x++ ) { 53 | 54 | char token = content.charAt(x); 55 | 56 | if ( isEscaping ) { 57 | currentItem.append( token ); 58 | isEscaping = false; 59 | } else { 60 | if ( isQuoted ) { 61 | switch (token) { 62 | case '"': 63 | isQuoted = false; 64 | wasQuoted = true; 65 | break; 66 | case '\\': 67 | isEscaping = true; 68 | break; 69 | default: 70 | currentItem.append(token); 71 | } 72 | } else { 73 | switch (token) { 74 | case '\\': 75 | isEscaping = true; 76 | break; 77 | case ',': 78 | addItem(context, items, currentItem, wasQuoted); 79 | currentItem = new StringBuilder(); 80 | wasQuoted = false; 81 | break; 82 | case '"': 83 | isQuoted = true; 84 | break; 85 | case '{': 86 | RubyArray internalItems = RubyArray.newArray(context.getRuntime()); 87 | x = parseArrayContents( context, internalItems, content, x + 1 ); 88 | items.add(internalItems); 89 | break; 90 | case '}': 91 | addItem(context, items, currentItem, wasQuoted); 92 | return x; 93 | default: 94 | currentItem.append(token); 95 | } 96 | } 97 | } 98 | 99 | } 100 | 101 | return x; 102 | } 103 | 104 | private static void addItem( ThreadContext context, RubyArray items, StringBuilder builder, boolean quoted ) { 105 | String value = builder.toString(); 106 | 107 | if ( !quoted && value.length() == 0 ) { 108 | return; 109 | } 110 | 111 | if ( !quoted && "NULL".equalsIgnoreCase( value ) ) { 112 | items.add(context.getRuntime().getNil()); 113 | } else { 114 | items.add(RubyString.newString( context.getRuntime(), value )); 115 | } 116 | } 117 | 118 | } 119 | --------------------------------------------------------------------------------