diff --git a/IOStreams/CMakeLists.txt b/IOStreams/CMakeLists.txt index 88304e0..384e691 100644 --- a/IOStreams/CMakeLists.txt +++ b/IOStreams/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory( streamable.test ) add_subdirectory( LineTrackingStreambuf.test ) add_subdirectory( OutUnixFileBuf.test ) add_subdirectory( StackableStreambuf.test ) +add_subdirectory( Filter.test ) target_sources( alepha PRIVATE StackableStreambuf.cc diff --git a/IOStreams/Filter.h b/IOStreams/Filter.h new file mode 100644 index 0000000..64043d5 --- /dev/null +++ b/IOStreams/Filter.h @@ -0,0 +1,213 @@ +static_assert( __cplusplus > 2020'99 ); + +#pragma once + +#include + +#include + +#include + +#include + +#include + +namespace Alepha::Hydrogen::IOStreams ::detail:: Filter_m +{ + namespace C + { + const bool debug= false; + } + + inline namespace exports + { + class Emitter; + + Emitter nilEmitter(); + + class StreamFilter; + + class FilteringStreambuf; + + struct Filter_impl + { + std::unique_ptr< StreamFilter > filter; + + explicit + Filter_impl( std::unique_ptr< StreamFilter > filter ) + : filter( std::move( filter ) ) + {} + }; + using Filter= PushStack< Filter_impl >; + } + + // XXX: This should form the new heart of how stackable + // streambufs work. One writes StreamFilter objects. + // When adding a filter to a streambuf in a stack, we build + // a stack of filters. The stackable streambuf becomes + // a stackable filter streambuf. + + + class exports::Emitter + { + public: + struct Impl + { + virtual ~Impl()= default; + + virtual bool empty() const= 0; + + virtual char next()= 0; + }; + + private: + std::unique_ptr< Impl > pimpl; + + Impl &impl() { return *pimpl; } + const Impl &impl() const { return *pimpl; } + + public: + explicit + Emitter( std::unique_ptr< Impl > pimpl ) + : pimpl( std::move( pimpl ) ) {} + + bool empty() const { return impl().empty(); } + + char next() { return impl().next(); } + + void + drain( std::ostream &out ) + { + while( not empty() ) + { + out << next(); + } + } + }; + + inline Emitter + exports::nilEmitter() + { + struct NilImpl : Emitter::Impl + { + bool empty() const final { return true; } + char next() final { throw std::out_of_range( "" ); } + }; + + return Emitter{ std::make_unique< NilImpl >() }; + } + + class exports::StreamFilter + : boost::noncopyable + { + public: + virtual ~StreamFilter()= default; + + virtual Emitter nextChar( char ch )= 0; + + virtual Emitter + nextChar( std::string_view chars ) + { + std::size_t amount= 0; + for( const char ch: chars ) + { + ++amount; + auto emitter= nextChar( ch ); + if( not emitter.empty() ) + { + return emitter; + } + } + + return nilEmitter(); + } + }; + + class exports::FilteringStreambuf + : public virtual IOStreams::StackableStreambuf + { + private: + static const std::size_t limit= 256; + + std::unique_ptr< StreamFilter > filter; + + Emitter nextInput= nilEmitter(); + std::string inBuffer; + std::string outBuffer= std::string( int( limit ), '\0' ); + + public: + explicit + FilteringStreambuf( std::ios &stream, std::unique_ptr< StreamFilter > filter ) + : StackableStreambuf( stream ), filter( std::move( filter ) ) + {} + + private: + void drain() final {} + void writeChar( char ) final { throw "Unimpl"; } + + int + underflow() override + { + if( C::debug ) error() << "Underflow called..." << std::endl; + inBuffer.clear(); + while( inBuffer.size() < limit ) + { + if( nextInput.empty() ) + { + const auto next= underlying_sbumpc(); + if( next == EOF ) break; + nextInput= filter->nextChar( next ); + continue; + } + inBuffer.push_back( nextInput.next() ); + } + setg( inBuffer.data(), inBuffer.data(), inBuffer.data() + inBuffer.size() ); + if( inBuffer.empty() ) return EOF; + + if( C::debug ) error() << "Underflow returning: " << inBuffer.front(); + return inBuffer.front(); + } + + int + overflow( const int nextChar ) override + { + sync(); + + if( nextChar != EOF ) + { + Emitter emitter= filter->nextChar( nextChar ); + emitter.drain( out() ); + } + + return 1; + } + + int + sync() override + { + for( const auto next: std::string_view{ pbase(), pptr() } ) + { + Emitter emitter= filter->nextChar( next ); + emitter.drain( out() ); + } + out().rdbuf()->pubsync(); + + std::fill_n( begin( outBuffer ), limit, 0 ); + setg( outBuffer.data(), outBuffer.data(), outBuffer.data() + outBuffer.size() ); + + return 1; + } + }; + + inline void + build_streambuf( std::ios &ios, Filter &¶ms ) + { + new FilteringStreambuf( ios, std::move( params.filter ) ); + } +} + +namespace Alepha::Hydrogen::IOStreams::inline exports::inline Filter_m +{ + using namespace detail::Filter_m::exports; +} + diff --git a/IOStreams/Filter.test/0.cc b/IOStreams/Filter.test/0.cc new file mode 100644 index 0000000..3ccd74b --- /dev/null +++ b/IOStreams/Filter.test/0.cc @@ -0,0 +1,128 @@ +static_assert( __cplusplus > 2020'99 ); + +#include "../Filter.h" + +#include +#include + +#include +#include + +static auto init= Alepha::Utility::enroll <=[] +{ + using namespace std::literals::string_literals; + using namespace Alepha::Testing::literals; + + using namespace Alepha::Testing::exports; + + []( const std::string &s ) + { + std::stringstream iss{ s }; + + //iss >> Filter{ std::make_unique< CapitalizationFilter >() }; + + std::string rv; + iss << "bob"; + int x; + iss >> x; + }; + + "Simple Filter"_test <=TableTest + < + []( const std::string &s ) + -> std::string + { + using namespace Alepha::IOStreams::exports::Filter_m; + struct CapitalizationFilter : StreamFilter + { + public: + Emitter + nextChar( const char ch ) override + { + struct EmitterImpl + : public Emitter::Impl + { + char ch; + bool read= false; + + explicit EmitterImpl( const char ch ) : ch( ch ) {} + + bool empty() const final { return read; } + char + next() final + { + read= true; + std::cerr << "Emitted `" << ch << "`" << std::endl; + return ch; + } + }; + + std::cerr << "Emitter built for `" << ch << "`" << std::endl; + return Emitter{ std::make_unique< EmitterImpl >( ::toupper( ch ) ) }; + } + }; + + std::ostringstream oss; + + oss << Filter{ std::make_unique< CapitalizationFilter >() }; + + oss << s; + + oss << Alepha::IOStreams::PopStack{}; + + return oss.str(); + } + > + ::Cases + { + { "Hello", { "hello" }, "HELLO" }, + }; + + auto streamWriter= []( const std::string &s ) + { + using namespace Alepha::IOStreams::exports::Filter_m; + struct CapitalizationFilter : StreamFilter + { + public: + Emitter + nextChar( const char ch ) override + { + struct EmitterImpl + : public Emitter::Impl + { + char ch; + bool read= false; + + explicit EmitterImpl( const char ch ) : ch( ch ) {} + + bool empty() const final { return read; } + char + next() final + { + read= true; + std::cerr << "Emitted `" << ch << "`" << std::endl; + return ch; + } + }; + std::cerr << "Emitter built for `" << ch << "`" << std::endl; + return Emitter{ std::make_unique< EmitterImpl >( ::toupper( ch ) ) }; + } + }; + + std::stringstream iss{ s }; + + iss >> Filter{ std::make_unique< CapitalizationFilter >() }; + + std::string rv; + iss >> rv; + + iss >> Alepha::IOStreams::PopStack{}; + + return rv; + }; + + "Simple Input Filter"_test <=TableTest< streamWriter >::Cases + { + { "Hello", { "hello" }, "HELLO" }, + }; +}; diff --git a/IOStreams/Filter.test/CMakeLists.txt b/IOStreams/Filter.test/CMakeLists.txt new file mode 100644 index 0000000..b099603 --- /dev/null +++ b/IOStreams/Filter.test/CMakeLists.txt @@ -0,0 +1 @@ +unit_test( 0 )